初始提交
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# ShiErFeng 项目测试套件
|
||||
|
||||
这是一个为施尔丰项目创建的完整测试套件,使用 Playwright 进行端到端测试。测试环境已禁用验证码功能,以简化测试流程。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 使用默认方式手动启动前后端服务或`Docker Compose`自动启动项目前后端服务
|
||||
- 测试环境已禁用验证码功能
|
||||
- 验证登录流程和认证机制
|
||||
- 测试所有受保护的页面功能
|
||||
- 验证未登录用户访问受保护页面时的重定向行为
|
||||
|
||||
## 依赖安装
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
playwright install
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方式一:默认方法
|
||||
|
||||
#### 启动前端
|
||||
|
||||
```bash
|
||||
cd shierfeng-fastapi-frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### 启动后端
|
||||
|
||||
```bash
|
||||
cd shierfeng-fastapi-backend
|
||||
pip install -r requirements.txt
|
||||
python app.py --env=dev
|
||||
```
|
||||
|
||||
#### 运行测试
|
||||
|
||||
```bash
|
||||
cd shierfeng-fastapi-test
|
||||
pip install -r requirements.txt
|
||||
python -m pytest -v
|
||||
```
|
||||
|
||||
### 方式二:使用Docker
|
||||
|
||||
#### 进入测试目录
|
||||
|
||||
```bash
|
||||
cd shierfeng-fastapi-test
|
||||
```
|
||||
|
||||
#### 启动 Docker 服务
|
||||
|
||||
```bash
|
||||
# MySQL版本
|
||||
docker compose -f docker-compose.test.my.yml up -d --build
|
||||
# PostgreSQL版本
|
||||
docker compose -f docker-compose.test.pg.yml up -d --build
|
||||
```
|
||||
|
||||
#### 运行测试
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python -m pytest -v
|
||||
```
|
||||
|
||||
## 测试内容
|
||||
|
||||
### 登录测试
|
||||
|
||||
- 验证登录页面正常加载
|
||||
- 验证登录流程(测试环境已禁用验证码)
|
||||
- 测试认证后的页面访问
|
||||
|
||||
### 页面访问和功能测试
|
||||
|
||||
- 仪表盘页面
|
||||
- 用户管理页面
|
||||
- 角色管理页面
|
||||
- 菜单管理页面
|
||||
- 部门管理页面
|
||||
- 岗位管理页面
|
||||
- 字典管理页面
|
||||
- 参数配置页面
|
||||
- 通知公告页面
|
||||
- 日志管理页面(操作日志、登录日志)
|
||||
- 在线用户页面
|
||||
- 定时任务页面
|
||||
- 服务监控页面
|
||||
- 数据监控页面
|
||||
- 缓存监控页面
|
||||
- 缓存列表页面
|
||||
- 代码生成页面
|
||||
- 系统接口页面
|
||||
|
||||
### 认证测试
|
||||
|
||||
- 验证未登录用户访问受保护页面时被重定向到登录页
|
||||
- 验证登录后可以访问受保护页面
|
||||
|
||||
## 配置说明
|
||||
|
||||
使用 `docker-compose.test.my.yml`或`docker-compose.test.pg.yml`启动服务,默认前端端口为 `80`,后端端口为 `9099`。测试环境已禁用验证码功能。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保系统已安装 Docker 和 Docker Compose
|
||||
2. 确保端口 `80` 和 `9099` 未被占用
|
||||
3. 首次运行时 Docker 镜像构建可能需要几分钟时间
|
||||
4. 测试使用默认管理员账户:用户名 `admin`,密码 `admin123`
|
||||
@@ -0,0 +1,87 @@
|
||||
import re
|
||||
|
||||
from playwright.async_api._context_manager import PlaywrightContextManager
|
||||
|
||||
from common.login_helper import LoginHelper
|
||||
|
||||
|
||||
class BasePageTest:
|
||||
browser = None
|
||||
context = None
|
||||
page = None
|
||||
token = None
|
||||
|
||||
async def setup(self, playwright: PlaywrightContextManager) -> None:
|
||||
"""初始化浏览器和登录"""
|
||||
# 首先登录获取token
|
||||
helper = LoginHelper()
|
||||
self.token = helper.login(username='admin', password='admin123')
|
||||
assert self.token is not None, '登录应该成功'
|
||||
|
||||
# 启动浏览器
|
||||
self.browser = await playwright.chromium.launch(headless=True)
|
||||
self.context = await self.browser.new_context()
|
||||
# 设置认证token
|
||||
await self.context.add_cookies(
|
||||
[
|
||||
{
|
||||
'name': 'Admin-Token',
|
||||
'value': self.token,
|
||||
'domain': 'localhost',
|
||||
'path': '/',
|
||||
'httpOnly': False,
|
||||
'secure': False,
|
||||
}
|
||||
]
|
||||
)
|
||||
self.page = await self.context.new_page()
|
||||
|
||||
async def teardown(self) -> None:
|
||||
"""清理资源"""
|
||||
if self.context:
|
||||
await self.context.close()
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
|
||||
async def goto_page(self, url: str) -> None:
|
||||
"""访问指定页面"""
|
||||
await self.page.goto(url)
|
||||
|
||||
async def wait_for_page_title(self, title_text: str, timeout: int = 10000) -> None:
|
||||
"""等待页面标题出现"""
|
||||
await self.page.wait_for_selector(f'div:has-text("{title_text}")', timeout=timeout)
|
||||
page_title = await self.page.inner_text(f'div:has-text("{title_text}")')
|
||||
assert title_text in page_title
|
||||
|
||||
async def wait_for_selector(self, selector: str, timeout: int = 10000) -> None:
|
||||
"""等待选择器出现"""
|
||||
await self.page.wait_for_selector(selector, timeout=timeout)
|
||||
|
||||
async def query_selector(self, selector: str) -> any:
|
||||
"""查询选择器元素"""
|
||||
return await self.page.query_selector(selector)
|
||||
|
||||
async def click_button(self, button_text: str) -> None:
|
||||
"""点击按钮"""
|
||||
button = await self.page.wait_for_selector(f'text="{button_text}"', timeout=5000)
|
||||
await button.click()
|
||||
|
||||
async def fill_input(self, selector: str, value: str) -> None:
|
||||
"""填充输入框"""
|
||||
await self.page.fill(selector, value)
|
||||
|
||||
async def get_text_content(self, selector: str) -> str:
|
||||
"""获取元素文本内容"""
|
||||
element = await self.page.query_selector(selector)
|
||||
if element:
|
||||
return await element.text_content()
|
||||
return None
|
||||
|
||||
async def get_table_total_rows(self) -> int:
|
||||
"""获取表格总行数"""
|
||||
element = self.page.locator('span.el-pagination__total').first
|
||||
text = await element.text_content()
|
||||
|
||||
# 提取数字
|
||||
number = re.search(r'\d+', text).group()
|
||||
return int(number)
|
||||
@@ -0,0 +1,3 @@
|
||||
class Config:
|
||||
frontend_url = 'http://localhost:80'
|
||||
backend_url = 'http://localhost:9099'
|
||||
@@ -0,0 +1,57 @@
|
||||
import requests
|
||||
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class LoginHelper:
|
||||
"""
|
||||
登录辅助类
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str = Config.frontend_url) -> None:
|
||||
self.base_url = base_url
|
||||
self.session = requests.Session()
|
||||
|
||||
def login(self, username: str = 'admin', password: str = 'admin123', max_retries: int = 3) -> str | None:
|
||||
"""
|
||||
执行登录操作(在测试环境中,验证码已禁用)
|
||||
"""
|
||||
for _attempt in range(max_retries):
|
||||
# 在测试环境中,验证码已禁用,所以直接登录
|
||||
login_data = {'username': username, 'password': password}
|
||||
|
||||
headers = {'Content-Type': 'application/x-www-form-urlencoded', 'Referer': f'{self.base_url}/login'}
|
||||
|
||||
response = self.session.post(f'{Config.backend_url}/login', data=login_data, headers=headers)
|
||||
|
||||
http_ok = 200
|
||||
if response.status_code == http_ok:
|
||||
result = response.json()
|
||||
# 检查登录是否成功 - 可能是 code=200 或 success=true
|
||||
if result.get('code') == http_ok or result.get('success'):
|
||||
# 尝试从不同可能的位置获取token
|
||||
token = result.get('token') # 直接在根级别
|
||||
if not token:
|
||||
token = result.get('data', {}).get('token') # 在data对象内
|
||||
|
||||
if token:
|
||||
print('登录成功')
|
||||
return token
|
||||
print(f'登录失败: 响应中未找到token - {result}')
|
||||
else:
|
||||
print(f'登录失败: {result.get("msg", "未知错误")}')
|
||||
else:
|
||||
print(f'登录请求失败: 状态码 {response.status_code}')
|
||||
|
||||
print('登录失败,已达到最大重试次数')
|
||||
return None
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == '__main__':
|
||||
helper = LoginHelper()
|
||||
token = helper.login()
|
||||
if token:
|
||||
print(f'获取到token: {token}')
|
||||
else:
|
||||
print('登录失败')
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 用于测试环境的SQL脚本,禁用验证码功能
|
||||
|
||||
-- 更新验证码配置为禁用状态
|
||||
UPDATE sys_config SET config_value = 'false' WHERE config_key = 'sys.account.captchaEnabled';
|
||||
@@ -0,0 +1,77 @@
|
||||
services:
|
||||
# 前端服务
|
||||
shierfeng-frontend:
|
||||
build:
|
||||
context: ../shierfeng-fastapi-frontend
|
||||
dockerfile: Dockerfile
|
||||
image: shierfeng-frontend:latest
|
||||
container_name: shierfeng-frontend-test
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ../shierfeng-fastapi-frontend/bin/nginx.dockermy.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- shierfeng-backend-my
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
|
||||
# 后端服务(MySQL版本)
|
||||
shierfeng-backend-my:
|
||||
build:
|
||||
context: ../shierfeng-fastapi-backend
|
||||
dockerfile: Dockerfile.my
|
||||
image: shierfeng-backend-my:latest
|
||||
container_name: shierfeng-backend-my-test
|
||||
ports:
|
||||
- "9099:9099"
|
||||
environment:
|
||||
TRANSPORT_CRYPTO_ENABLED: "false"
|
||||
TRANSPORT_CRYPTO_MODE: "off"
|
||||
depends_on:
|
||||
shierfeng-mysql:
|
||||
condition: service_healthy
|
||||
shierfeng-redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
|
||||
# MySQL服务
|
||||
shierfeng-mysql:
|
||||
image: mysql:8.0
|
||||
container_name: shierfeng-mysql-test
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: shierfeng-fastapi
|
||||
ports:
|
||||
- "13307:3306"
|
||||
volumes:
|
||||
- ../shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql:/docker-entrypoint-initdb.d/01-shierfeng-fastapi.sql
|
||||
- ./disable_captcha.sql:/docker-entrypoint-initdb.d/02-disable-captcha.sql
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --skip-character-set-client-handshake=1
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
healthcheck:
|
||||
test: ["CMD", "bash", "-c", "mysqladmin ping -h localhost -u root -proot && mysql -h localhost -u root -proot -e 'SELECT 1'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# Redis服务
|
||||
shierfeng-redis:
|
||||
image: redis:latest
|
||||
container_name: shierfeng-redis-test
|
||||
ports:
|
||||
- "16380:6379"
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# 网络配置
|
||||
networks:
|
||||
shierfeng-network-test:
|
||||
name: shierfeng-network-test
|
||||
driver: bridge
|
||||
@@ -0,0 +1,77 @@
|
||||
services:
|
||||
# 前端服务
|
||||
shierfeng-frontend:
|
||||
build:
|
||||
context: ../shierfeng-fastapi-frontend
|
||||
dockerfile: Dockerfile
|
||||
image: shierfeng-frontend:latest
|
||||
container_name: shierfeng-frontend-test
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ../shierfeng-fastapi-frontend/bin/nginx.dockerpg.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- shierfeng-backend-pg
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
|
||||
# 后端服务(PostgreSQL版本)
|
||||
shierfeng-backend-pg:
|
||||
build:
|
||||
context: ../shierfeng-fastapi-backend
|
||||
dockerfile: Dockerfile.pg
|
||||
image: shierfeng-backend-pg:latest
|
||||
container_name: shierfeng-backend-pg-test
|
||||
ports:
|
||||
- "9099:9099"
|
||||
environment:
|
||||
TRANSPORT_CRYPTO_ENABLED: "false"
|
||||
TRANSPORT_CRYPTO_MODE: "off"
|
||||
depends_on:
|
||||
shierfeng-pg:
|
||||
condition: service_healthy
|
||||
shierfeng-redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
|
||||
# PostgreSQL服务
|
||||
shierfeng-pg:
|
||||
image: postgres:14
|
||||
container_name: shierfeng-pg-test
|
||||
environment:
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: shierfeng-fastapi
|
||||
POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate=C --lc-ctype=C
|
||||
ports:
|
||||
- "15433:5432"
|
||||
volumes:
|
||||
- ../shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql:/docker-entrypoint-initdb.d/01-shierfeng-fastapi-pg.sql
|
||||
- ./disable_captcha.sql:/docker-entrypoint-initdb.d/02-disable-captcha.sql
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# Redis服务
|
||||
shierfeng-redis:
|
||||
image: redis:latest
|
||||
container_name: shierfeng-redis-test
|
||||
ports:
|
||||
- "16381:6379"
|
||||
networks:
|
||||
- shierfeng-network-test
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
# 网络配置
|
||||
networks:
|
||||
shierfeng-network-test:
|
||||
name: shierfeng-network-test
|
||||
driver: bridge
|
||||
@@ -0,0 +1,81 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright, expect
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class CacheListTest(BasePageTest):
|
||||
"""缓存列表测试类"""
|
||||
|
||||
async def test_cache_list(self) -> None:
|
||||
"""测试缓存列表页面"""
|
||||
await self.page.goto(Config.frontend_url + '/monitor/cacheList')
|
||||
await self.page.wait_for_load_state('networkidle')
|
||||
|
||||
# 1. 缓存列表 - 点击 sys_config
|
||||
await self.page.wait_for_selector('text=缓存列表')
|
||||
|
||||
# 等待 sys_config 出现并点击
|
||||
sys_config_row = self.page.locator('.el-card', has_text='缓存列表').locator('tr', has_text='sys_config')
|
||||
await sys_config_row.wait_for()
|
||||
await sys_config_row.click()
|
||||
|
||||
# 2. 键名列表 - 点击 sys.account.captchaEnabled
|
||||
await self.page.wait_for_selector('text=键名列表')
|
||||
|
||||
# 等待 sys.account.captchaEnabled 出现并点击
|
||||
captcha_row = self.page.locator('.el-card', has_text='键名列表').locator(
|
||||
'tr', has_text='sys.account.captchaEnabled'
|
||||
)
|
||||
await captcha_row.wait_for()
|
||||
await captcha_row.click()
|
||||
|
||||
# 3. 验证缓存内容为 false
|
||||
await self.page.wait_for_selector('text=缓存内容')
|
||||
|
||||
# 等待数据加载
|
||||
await self.page.wait_for_timeout(2000)
|
||||
|
||||
# 获取内容
|
||||
# 缓存内容显示在一个 textarea 中
|
||||
content_area = self.page.locator('div.el-form-item', has_text='缓存内容:').locator('textarea')
|
||||
|
||||
# 也可以尝试直接获取 .el-form-item__content 的文本,如果 textarea 不可交互
|
||||
if await content_area.count() > 0:
|
||||
# 尝试等待内容不为空
|
||||
try:
|
||||
await expect(content_area).not_to_be_empty(timeout=5000)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
value = await content_area.input_value()
|
||||
print(f'Cache content value: {value}')
|
||||
|
||||
# 如果为空,可能需要重新点击一下
|
||||
if not value:
|
||||
print('Value is empty, trying to click key again')
|
||||
await captcha_row.click()
|
||||
await self.page.wait_for_timeout(2000)
|
||||
value = await content_area.input_value()
|
||||
print(f'Cache content value after retry: {value}')
|
||||
|
||||
assert 'false' in value.lower(), f"Expected 'false' in cache content, got: {value}"
|
||||
else:
|
||||
# 如果不是 textarea,尝试获取文本
|
||||
content_div = self.page.locator('div.el-form-item', has_text='缓存内容:').locator('.el-form-item__content')
|
||||
value = await content_div.text_content()
|
||||
print(f'Cache content text: {value}')
|
||||
assert 'false' in value.lower(), f"Expected 'false' in cache content, got: {value}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_list_page() -> None:
|
||||
"""测试缓存列表页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = CacheListTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_cache_list()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class CacheMonitorTest(BasePageTest):
|
||||
"""缓存监控测试类"""
|
||||
|
||||
async def test_cache_monitor(self) -> None:
|
||||
"""测试缓存监控页面"""
|
||||
await self.page.goto(Config.frontend_url + '/monitor/cache')
|
||||
await self.page.wait_for_load_state('networkidle')
|
||||
|
||||
# 验证基本信息
|
||||
await self.page.wait_for_selector('text=基本信息')
|
||||
await self.page.wait_for_selector('text=Redis版本')
|
||||
await self.page.wait_for_selector('text=运行模式')
|
||||
|
||||
# 验证命令统计
|
||||
await self.page.wait_for_selector('text=命令统计')
|
||||
|
||||
# 验证内存信息
|
||||
await self.page.wait_for_selector('text=内存信息')
|
||||
|
||||
# 验证端口为 6379
|
||||
port_row = self.page.locator('tr', has_text='端口')
|
||||
try:
|
||||
await port_row.wait_for(timeout=5000)
|
||||
text = await port_row.text_content()
|
||||
assert '6379' in text, f"Expected port '6379' in row, but got: {text}"
|
||||
except Exception:
|
||||
print("Warning: '端口' row not found, checking page content")
|
||||
content = await self.page.content()
|
||||
assert '6379' in content, "Port '6379' not found in page content"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_monitor_page() -> None:
|
||||
"""测试缓存监控页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = CacheMonitorTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_cache_monitor()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,185 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class JobManagementTest(BasePageTest):
|
||||
"""定时任务管理测试"""
|
||||
|
||||
def generate_job_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = datetime.now().strftime('%H%M%S')
|
||||
return {
|
||||
'job_name': f'测试任务_{timestamp}',
|
||||
'job_group': '默认',
|
||||
'job_executor': '进程池',
|
||||
'invoke_target': 'module_task.scheduler_test.job', # 必须为这个值
|
||||
'cron_expression': '0/2 * * * * ?', # 每2秒执行一次
|
||||
'new_invoke_target': 'module_task.scheduler_test.job', # 修改时也用这个
|
||||
'job_name_edit': f'测试任务_{timestamp}_edit',
|
||||
}
|
||||
|
||||
async def create_job(
|
||||
self, job_name: str, job_group: str, job_executor: str, invoke_target: str, cron_expression: str
|
||||
) -> None:
|
||||
"""创建定时任务"""
|
||||
await self.page.get_by_role('button', name='新增').click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写表单
|
||||
await dialog.get_by_role('textbox', name='任务名称').fill(job_name)
|
||||
|
||||
# 选择任务分组
|
||||
await dialog.locator("label:has-text('任务分组') + div .el-select").click()
|
||||
await self.page.get_by_role('option', name=job_group).click()
|
||||
|
||||
# 选择任务执行器
|
||||
await dialog.locator("label:has-text('任务执行器') + div .el-select").click()
|
||||
await self.page.get_by_role('option', name=job_executor).click()
|
||||
|
||||
# 填写调用方法
|
||||
await dialog.get_by_role('textbox', name='调用方法').fill(invoke_target)
|
||||
|
||||
# 填写cron表达式
|
||||
await dialog.get_by_role('textbox', name='cron表达式').fill(cron_expression)
|
||||
|
||||
# 策略:如果"失败"策略等有默认值,就不管了
|
||||
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
await self.wait_for_selector("div:has-text('新增成功')", timeout=10000)
|
||||
|
||||
async def search_job(self, job_name: str) -> None:
|
||||
"""搜索任务"""
|
||||
# 我们可以限制在 form 里搜索
|
||||
form = self.page.locator('form').first
|
||||
await form.get_by_role('textbox', name='任务名称').fill(job_name)
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
await self.page.wait_for_timeout(1000) # 等待搜索结果
|
||||
|
||||
async def edit_job(self, job_name: str, new_invoke_target: str) -> None:
|
||||
"""修改任务"""
|
||||
await self.search_job(job_name)
|
||||
|
||||
# 点击修改 (在操作列)
|
||||
row = self.page.locator('tbody tr').first
|
||||
# 操作列按钮顺序: 修改, 删除, 执行一次, 详细, 日志
|
||||
await row.locator('button').nth(0).click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 修改调用方法
|
||||
await dialog.get_by_role('textbox', name='调用方法').fill(new_invoke_target)
|
||||
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
await self.wait_for_selector("div:has-text('修改成功')", timeout=10000)
|
||||
|
||||
async def toggle_job_status(self, job_name: str) -> None:
|
||||
"""切换任务状态"""
|
||||
await self.search_job(job_name)
|
||||
|
||||
row = self.page.locator('tbody tr').first
|
||||
# 切换开关
|
||||
# 需要点击可见的 .el-switch 或 .el-switch__core
|
||||
await row.locator('.el-switch').click()
|
||||
|
||||
# 确认切换
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
await self.wait_for_selector("div:has-text('成功')", timeout=10000)
|
||||
|
||||
async def run_job_once(self, job_name: str) -> None:
|
||||
"""执行一次任务"""
|
||||
await self.search_job(job_name)
|
||||
|
||||
row = self.page.locator('tbody tr').first
|
||||
# 点击执行一次 (第3个按钮)
|
||||
await row.locator('button').nth(2).click()
|
||||
|
||||
# 确认执行
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector("div:has-text('执行成功')", timeout=10000)
|
||||
|
||||
async def view_job_log(self, job_name: str) -> None:
|
||||
"""查看调度日志"""
|
||||
# 可以点击顶部的 "日志" 按钮,也可以点击行的 "调度日志"
|
||||
# 我们点击行的 "调度日志" (icon="Operation")
|
||||
await self.search_job(job_name)
|
||||
row = self.page.locator('tbody tr').first
|
||||
|
||||
log_btn = row.locator('button').nth(4)
|
||||
await log_btn.click()
|
||||
|
||||
# 等待跳转到日志页面 (实际上是 router push 到 /monitor/job-log)
|
||||
# 检查 URL
|
||||
await self.page.wait_for_url('**/monitor/job-log**')
|
||||
|
||||
# 验证在日志页面
|
||||
await self.wait_for_selector('text=调度日志')
|
||||
|
||||
# 关闭返回 (点击 "关闭" 按钮)
|
||||
await self.page.get_by_role('button', name='关闭').click()
|
||||
|
||||
# 等待返回任务列表
|
||||
await self.page.wait_for_url('**/monitor/job')
|
||||
|
||||
async def delete_job(self, job_name: str) -> None:
|
||||
"""删除任务"""
|
||||
await self.search_job(job_name)
|
||||
|
||||
row = self.page.locator('tbody tr').first
|
||||
# 点击删除 (第2个按钮)
|
||||
await row.locator('button').nth(1).click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector("div:has-text('删除成功')", timeout=10000)
|
||||
|
||||
async def test_job_crud_operations(self) -> None:
|
||||
"""测试定时任务管理全流程"""
|
||||
data = self.generate_job_data()
|
||||
|
||||
# 1. 进入定时任务页面
|
||||
await self.goto_page(Config.frontend_url + '/monitor/job')
|
||||
await self.wait_for_selector('text=任务名称')
|
||||
|
||||
# 2. 创建任务
|
||||
await self.create_job(
|
||||
data['job_name'], data['job_group'], data['job_executor'], data['invoke_target'], data['cron_expression']
|
||||
)
|
||||
|
||||
# 3. 修改任务 (修改调用方法)
|
||||
await self.edit_job(data['job_name'], data['new_invoke_target'])
|
||||
|
||||
# 4. 切换状态 (开启/关闭)
|
||||
await self.toggle_job_status(data['job_name']) # 开启
|
||||
await self.page.wait_for_timeout(1000)
|
||||
await self.toggle_job_status(data['job_name']) # 关闭
|
||||
|
||||
# 5. 执行一次
|
||||
await self.run_job_once(data['job_name'])
|
||||
|
||||
# 6. 查看日志
|
||||
await self.view_job_log(data['job_name'])
|
||||
|
||||
# 7. 删除任务
|
||||
await self.delete_job(data['job_name'])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_job_management_page() -> None:
|
||||
"""测试定时任务管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = JobManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_job_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,73 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class OnlineUserTest(BasePageTest):
|
||||
"""在线用户测试类"""
|
||||
|
||||
async def test_online_user_operations(self) -> None:
|
||||
"""测试在线用户强退"""
|
||||
# 1. 模拟 niangao 用户登录以产生在线会话
|
||||
# 使用一个新的上下文来模拟另一个用户的登录
|
||||
niangao_context = await self.browser.new_context()
|
||||
niangao_page = await niangao_context.new_page()
|
||||
|
||||
# 登录 niangao
|
||||
try:
|
||||
await niangao_page.goto(Config.frontend_url + '/login')
|
||||
await niangao_page.locator('div.el-form-item', has_text='账号').locator('input').fill('niangao')
|
||||
await niangao_page.locator('div.el-form-item', has_text='密码').locator('input').fill('admin123')
|
||||
await niangao_page.get_by_role('button', name='登 录').click()
|
||||
# 等待登录成功
|
||||
await niangao_page.wait_for_url('**/index')
|
||||
except Exception as e:
|
||||
print(f'Niangao login failed: {e}. Trying admin123')
|
||||
try:
|
||||
await niangao_page.locator('div.el-form-item', has_text='密码').locator('input').fill('123456')
|
||||
await niangao_page.get_by_role('button', name='登 录').click()
|
||||
await niangao_page.wait_for_url('**/index')
|
||||
except Exception as e2:
|
||||
print(f'Niangao login failed again: {e2}')
|
||||
|
||||
# 2. 切换回 admin (self.page) 进行操作
|
||||
await self.page.goto(Config.frontend_url + '/monitor/online')
|
||||
await self.page.wait_for_load_state('networkidle')
|
||||
|
||||
# 3. 搜索 niangao 用户
|
||||
await self.page.locator('div.el-form-item', has_text='用户名称').locator('input').fill('niangao')
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
# 4. 强退操作
|
||||
# 检查是否有数据
|
||||
rows = self.page.locator('.el-table__body tr')
|
||||
count = await rows.count()
|
||||
if count > 0:
|
||||
# 点击强退
|
||||
await rows.first.get_by_role('button', name='强退').click()
|
||||
# 确认
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
# 验证
|
||||
await self.page.wait_for_selector('text=删除成功', timeout=3000)
|
||||
|
||||
# 再次搜索确认消失
|
||||
await self.page.wait_for_timeout(1000)
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
|
||||
# 关闭 niangao 的上下文
|
||||
await niangao_context.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_online_user_page() -> None:
|
||||
"""测试在线用户页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = OnlineUserTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_online_user_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class ServerMonitorTest(BasePageTest):
|
||||
"""服务监控测试类"""
|
||||
|
||||
async def test_server_monitor(self) -> None:
|
||||
"""测试服务监控页面"""
|
||||
await self.page.goto(Config.frontend_url + '/monitor/server')
|
||||
await self.page.wait_for_load_state('networkidle')
|
||||
|
||||
# 验证主要板块存在
|
||||
await self.page.wait_for_selector('text=CPU')
|
||||
await self.page.wait_for_selector('text=内存')
|
||||
await self.page.wait_for_selector('text=服务器信息')
|
||||
await self.page.wait_for_selector('text=Python解释器信息')
|
||||
await self.page.wait_for_selector('text=磁盘状态')
|
||||
|
||||
# 验证项目路径为 /app
|
||||
# 尝试在表格行中查找
|
||||
project_path_row = self.page.locator('tr', has_text='项目路径')
|
||||
try:
|
||||
await project_path_row.wait_for(timeout=5000)
|
||||
text = await project_path_row.text_content()
|
||||
assert '/app' in text, f"Expected project path '/app' in row, but got: {text}"
|
||||
except Exception:
|
||||
# 如果没找到行,尝试全局搜索
|
||||
print("Warning: '项目路径' row not found, checking page content")
|
||||
content = await self.page.content()
|
||||
assert '/app' in content, "Project path '/app' not found in page content"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_monitor_page() -> None:
|
||||
"""测试服务监控页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = ServerMonitorTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_server_monitor()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,5 @@
|
||||
playwright
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-playwright
|
||||
ruff
|
||||
@@ -0,0 +1,68 @@
|
||||
line-length = 120
|
||||
show-fixes = true
|
||||
target-version = "py310"
|
||||
unsafe-fixes = true
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
"FAST", # fastapi
|
||||
"ANN", # flake8-annotations
|
||||
"ASYNC", # flake8-async
|
||||
"B", # flake8-bugbear
|
||||
"A", # flake8-builtins
|
||||
"COM", # flake8-commas
|
||||
"C4", # flake8-comprehensions
|
||||
"ISC", # flake8-implicit-str-concat
|
||||
"ICN", # flake8-import-conventions
|
||||
"PIE", # flake8-pie
|
||||
"RSE", # flake8-raise
|
||||
"RET", # flake8-return
|
||||
"SIM", # flake8-simplify
|
||||
"TC", # flake8-type-checking
|
||||
"FLY", # flynt
|
||||
"I", # isort
|
||||
"N", # pep8-naming
|
||||
"PERF", # perflint
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"PGH", # pygrep-hooks
|
||||
"PL", # pylint
|
||||
"UP", # pyupgrade
|
||||
"RUF", # Ruff-specific rules
|
||||
]
|
||||
ignore = [
|
||||
"ANN002", # missing type annotation for *{name}
|
||||
"ANN003", # missing type annotation for **{name}
|
||||
"ANN401", # dynamically typed expressions (typing.Any) are disallowed in {name}
|
||||
"B008", # do not perform function calls in argument defaults
|
||||
"COM812", # trailing comma missing
|
||||
"RET504", # unnecessary assignment to {name} before return statement
|
||||
"SIM105", # use contextlib.suppress({exception}) instead of try-except-pass
|
||||
"C901", # too complex
|
||||
"N818", # exception name {name} should be named with an Error suffix
|
||||
"E501", # line too long
|
||||
"W191", # indentation contains tabs
|
||||
"RUF001", # string contains ambiguous
|
||||
"RUF002", # docstring contains ambiguous
|
||||
"RUF003", # comment contains ambiguous
|
||||
"RUF012", # mutable class attributes should be annotated with typing.ClassVar
|
||||
]
|
||||
|
||||
[lint.flake8-type-checking]
|
||||
runtime-evaluated-base-classes = ["pydantic.BaseModel", "sqlalchemy.orm.DeclarativeBase"]
|
||||
|
||||
[lint.isort]
|
||||
known-third-party = ["alembic"]
|
||||
|
||||
[lint.pylint]
|
||||
max-args = 10
|
||||
max-returns = 10
|
||||
|
||||
[lint.pyupgrade]
|
||||
# Preserve types, even if a file imports `from __future__ import annotations`.
|
||||
keep-runtime-typing = true
|
||||
|
||||
[format]
|
||||
docstring-code-format = true
|
||||
quote-style = "single"
|
||||
@@ -0,0 +1,165 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class ConfigManagementTest(BasePageTest):
|
||||
"""参数设置测试类"""
|
||||
|
||||
def generate_config_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = int(time.time())
|
||||
return {
|
||||
'config_name': f'test_config_{timestamp}',
|
||||
'config_key': f'test_config_key_{timestamp}',
|
||||
'config_value': f'test_config_value_{timestamp}',
|
||||
}
|
||||
|
||||
async def create_config(self, config_name: str, config_key: str, config_value: str) -> None:
|
||||
"""创建参数配置"""
|
||||
# 点击新增按钮
|
||||
await self.page.get_by_role('button', name='新增').first.click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写参数信息
|
||||
await dialog.get_by_role('textbox', name='参数名称').fill(config_name)
|
||||
await dialog.get_by_role('textbox', name='参数键名').fill(config_key)
|
||||
await dialog.get_by_role('textbox', name='参数键值').fill(config_value)
|
||||
|
||||
# 等待一段时间确保输入完成
|
||||
await self.page.wait_for_timeout(500)
|
||||
|
||||
# 选择参数状态 - 选择系统内置选项为N(否),避免后端限制删除
|
||||
# 在对话框中找到系统内置的单选按钮并选择
|
||||
await self.wait_for_selector('input[type="radio"]', timeout=5000)
|
||||
# 点击系统内置'否'选项
|
||||
# 根据codegen脚本,使用更精确的定位器
|
||||
await self.page.locator('label:nth-child(2) > .el-radio__input > .el-radio__inner').click()
|
||||
|
||||
# 等待一段时间确保选择完成
|
||||
await self.page.wait_for_timeout(500)
|
||||
|
||||
# 点击确认按钮
|
||||
confirm_button = self.page.get_by_role('button', name='确 定').first
|
||||
await confirm_button.click()
|
||||
|
||||
# 等待一段时间确保请求发送
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
# 等待新增成功提示
|
||||
await self.wait_for_selector('div:has-text("新增成功")', timeout=10000)
|
||||
|
||||
async def search_config(self, config_name: str, config_key: str = '') -> None:
|
||||
"""搜索参数配置"""
|
||||
# 在查询表单中输入参数信息进行查询
|
||||
await self.page.get_by_role('textbox', name='参数名称').fill(config_name)
|
||||
if config_key:
|
||||
await self.page.get_by_role('textbox', name='参数键名').fill(config_key)
|
||||
|
||||
# 点击搜索按钮
|
||||
await self.page.get_by_role('button', name='搜索').first.click()
|
||||
|
||||
# 等待搜索结果加载
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def edit_config(self, updated_value: str) -> None:
|
||||
"""编辑参数配置"""
|
||||
# 点击编辑按钮
|
||||
await self.page.locator('tbody').get_by_role('button', name='修改').nth(0).click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 修改参数键值
|
||||
await dialog.get_by_role('textbox', name='参数键值').fill(updated_value)
|
||||
|
||||
# 确认
|
||||
await self.page.get_by_role('button', name='确 定').first.click()
|
||||
|
||||
# 等待编辑成功提示
|
||||
await self.wait_for_selector('div:has-text("修改成功")', timeout=10000)
|
||||
|
||||
async def delete_config(self) -> None:
|
||||
"""删除参数配置"""
|
||||
# 点击删除按钮
|
||||
await self.page.locator('tbody').get_by_role('button', name='删除').nth(0).click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').first.click()
|
||||
|
||||
# 等待删除成功提示
|
||||
await self.wait_for_selector('div:has-text("删除成功")', timeout=10000)
|
||||
|
||||
# 重置查询表单,确保能看到所有参数
|
||||
reset_button = self.page.get_by_role('button', name='重置')
|
||||
await reset_button.click()
|
||||
|
||||
# 等待列表刷新
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def test_config_crud_operations(self) -> None:
|
||||
"""测试参数设置的增删查改功能"""
|
||||
# 访问参数设置页面
|
||||
await self.goto_page(Config.frontend_url + '/system/config')
|
||||
|
||||
# 等待页面加载完成
|
||||
await self.wait_for_page_title('参数设置', timeout=10000)
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
|
||||
# 记录初始参数数量
|
||||
initial_config_count = await self.get_table_total_rows()
|
||||
|
||||
# 生成测试数据
|
||||
data = self.generate_config_data()
|
||||
|
||||
# 1.新增
|
||||
await self.create_config(data['config_name'], data['config_key'], data['config_value'])
|
||||
|
||||
# 验证新增
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
new_config_count = await self.get_table_total_rows()
|
||||
assert new_config_count > initial_config_count, '新增参数后数量应该增加'
|
||||
|
||||
# 2.搜索
|
||||
await self.search_config(data['config_name'], data['config_key'])
|
||||
|
||||
# 验证搜索
|
||||
search_result_count = await self.get_table_total_rows()
|
||||
assert search_result_count >= 1, '搜索结果应该至少包含一个参数'
|
||||
|
||||
# 3.编辑
|
||||
await self.edit_config(f'updated_config_value_{time.time()}')
|
||||
|
||||
# 验证编辑
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
|
||||
# 重新搜索以验证修改结果
|
||||
await self.search_config(data['config_name'])
|
||||
|
||||
# 4.删除
|
||||
await self.delete_config()
|
||||
|
||||
# 验证删除
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
final_config_count = await self.get_table_total_rows()
|
||||
assert final_config_count == initial_config_count, '删除参数后数量应该恢复到初始值'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_management_page() -> None:
|
||||
"""测试参数设置页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = ConfigManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_config_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,135 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class DeptManagementTest(BasePageTest):
|
||||
"""部门管理页面测试类"""
|
||||
|
||||
def generate_dept_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = int(time.time())
|
||||
return {
|
||||
'dept_name': f'测试部门_{timestamp}',
|
||||
'order_num': '3',
|
||||
'leader_name': '年糕',
|
||||
}
|
||||
|
||||
async def create_dept(self, dept_name: str, order_num: str) -> None:
|
||||
"""创建部门"""
|
||||
# 点击新增按钮
|
||||
await self.page.get_by_role('button', name='新增').first.click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 上级部门
|
||||
# 使用 label 定位父级 form-item,再点击内部的 wrapper
|
||||
await dialog.locator('div.el-form-item').filter(has_text='上级部门').locator('.el-select__wrapper').click()
|
||||
# 选择 "集团总公司" (根节点)
|
||||
# 使用 .el-popper 定位下拉框中的内容
|
||||
await self.page.locator('.el-popper:visible').get_by_text('集团总公司').click()
|
||||
|
||||
# 填写部门名称
|
||||
await dialog.get_by_role('textbox', name='部门名称').fill(dept_name)
|
||||
|
||||
# 填写显示排序
|
||||
# el-input-number 通常是一个 spinbutton 或者 input
|
||||
await dialog.get_by_role('spinbutton', name='显示排序').fill(order_num)
|
||||
|
||||
# 点击确定
|
||||
await self.page.get_by_role('button', name='确 定').click()
|
||||
# 等待成功消息
|
||||
await self.wait_for_selector("div:has-text('成功')", timeout=10000)
|
||||
|
||||
async def search_dept(self, dept_name: str) -> None:
|
||||
"""搜索部门"""
|
||||
search_form = self.page.locator('form').first
|
||||
await search_form.get_by_role('textbox', name='部门名称').fill(dept_name)
|
||||
# 点击搜索按钮
|
||||
await self.page.get_by_role('button', name='搜索').first.click()
|
||||
|
||||
# 等待加载
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def edit_dept(self, leader_name: str) -> None:
|
||||
"""编辑部门"""
|
||||
# 点击修改按钮 (第一行)
|
||||
# 树形表格结构可能不同,但通常也是在 tbody 的 row 中
|
||||
await self.page.locator('tbody').get_by_role('button', name='修改').nth(0).click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 修改负责人
|
||||
await dialog.get_by_role('textbox', name='负责人').fill(leader_name)
|
||||
|
||||
# 确定
|
||||
await self.page.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector('div:has-text("修改成功")', timeout=10000)
|
||||
|
||||
async def delete_dept(self) -> None:
|
||||
"""删除部门"""
|
||||
# 点击删除按钮
|
||||
await self.page.locator('tbody').get_by_role('button', name='删除').nth(0).click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector('div:has-text("删除成功")', timeout=10000)
|
||||
|
||||
# 重置搜索
|
||||
await self.page.get_by_role('button', name='重置').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def test_dept_crud_operations(self) -> None:
|
||||
"""测试部门管理增删查改"""
|
||||
# 访问页面
|
||||
await self.goto_page(Config.frontend_url + '/system/dept')
|
||||
await self.wait_for_page_title('部门管理', timeout=10000)
|
||||
|
||||
# 生成测试数据
|
||||
data = self.generate_dept_data()
|
||||
|
||||
# 1. 新增
|
||||
await self.create_dept(data['dept_name'], data['order_num'])
|
||||
|
||||
# 验证新增
|
||||
await self.search_dept(data['dept_name'])
|
||||
# 树形表格,只要有行显示即可
|
||||
rows = await self.page.query_selector_all('.el-table__row')
|
||||
assert len(rows) >= 1, '新增后搜索应有结果'
|
||||
|
||||
# 2. 编辑
|
||||
await self.edit_dept(data['leader_name'])
|
||||
|
||||
# 重新搜索以验证修改结果
|
||||
await self.search_dept(data['dept_name'])
|
||||
|
||||
# 3. 删除
|
||||
await self.delete_dept()
|
||||
|
||||
# 验证删除
|
||||
await self.search_dept(data['dept_name'])
|
||||
rows_after = await self.page.query_selector_all('.el-table__row')
|
||||
assert len(rows_after) == 0, '删除后搜索应无结果'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dept_management_page() -> None:
|
||||
"""测试部门管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = DeptManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_dept_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,201 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class DictManagementTest(BasePageTest):
|
||||
"""字典管理测试类"""
|
||||
|
||||
def generate_dict_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = datetime.now().strftime('%H%M%S')
|
||||
return {
|
||||
'dict_name': f'测试字典_{timestamp}',
|
||||
'dict_type': f'test_dict_{timestamp}',
|
||||
'dict_data_label_1': '正常',
|
||||
'dict_data_value_1': '1',
|
||||
'dict_data_label_2': '异常',
|
||||
'dict_data_value_2': '2',
|
||||
'remark': f'备注_{timestamp}',
|
||||
}
|
||||
|
||||
async def create_dict_type(self, dict_name: str, dict_type: str) -> None:
|
||||
"""创建字典类型"""
|
||||
# 点击新增按钮
|
||||
await self.page.get_by_role('button', name='新增').first.click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写表单
|
||||
await dialog.get_by_role('textbox', name='字典名称').fill(dict_name)
|
||||
await dialog.get_by_role('textbox', name='字典类型').fill(dict_type)
|
||||
|
||||
# 提交
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 验证成功
|
||||
await self.wait_for_selector("div:has-text('新增成功')", timeout=10000)
|
||||
|
||||
async def search_dict_type(self, dict_name: str, dict_type: str) -> None:
|
||||
"""搜索字典类型"""
|
||||
form = self.page.locator('form').first
|
||||
await form.get_by_role('textbox', name='字典名称').fill(dict_name)
|
||||
await form.get_by_role('textbox', name='字典类型').fill(dict_type)
|
||||
# 点击搜索按钮
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def edit_dict_type(self, dict_name: str, remark: str) -> None:
|
||||
"""修改字典类型"""
|
||||
await self.search_dict_type(dict_name, '')
|
||||
|
||||
# 点击修改按钮
|
||||
row = self.page.locator('tbody tr').first
|
||||
await row.get_by_role('button', name='修改').click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
await dialog.get_by_role('textbox', name='备注').fill(remark)
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
await self.wait_for_selector("div:has-text('修改成功')", timeout=10000)
|
||||
|
||||
async def manage_dict_data(self, dict_type: str, data: dict) -> None:
|
||||
"""管理字典数据"""
|
||||
# 1. 进入字典数据页面
|
||||
# 点击字典类型链接
|
||||
row = self.page.locator('tbody tr').first
|
||||
await row.get_by_role('link', name=dict_type).click()
|
||||
|
||||
# 等待页面切换(Playwright在SPA中通常不需要处理多标签页,除非新开tab)
|
||||
# 这里是 router-link,应该是页内跳转
|
||||
# 检查是否到了字典数据页面
|
||||
# 或者等待URL变化,或者等待特定元素
|
||||
# data.vue 有 "关闭" 按钮
|
||||
await self.wait_for_selector("button:has-text('关闭')", timeout=5000)
|
||||
|
||||
# 2. 新增数据1 (正常)
|
||||
await self.create_dict_data(data['dict_data_label_1'], data['dict_data_value_1'], '1', '主要(primary)')
|
||||
|
||||
# 3. 新增数据2 (异常)
|
||||
await self.create_dict_data(data['dict_data_label_2'], data['dict_data_value_2'], '2', '危险(danger)')
|
||||
|
||||
# 4. 修改数据
|
||||
await self.edit_dict_data(data['dict_data_label_2'], '测试备注')
|
||||
|
||||
# 5. 删除数据
|
||||
await self.delete_dict_data(data['dict_data_label_2'])
|
||||
await self.delete_dict_data(data['dict_data_label_1'])
|
||||
|
||||
# 6. 关闭页面 (返回字典类型)
|
||||
await self.page.get_by_role('button', name='关闭', exact=True).click()
|
||||
# 等待返回
|
||||
await self.wait_for_selector('text=字典名称') # 假设返回后能看到原来的页面元素
|
||||
|
||||
async def create_dict_data(self, label: str, value: str, sort: str, list_class: str) -> None:
|
||||
"""创建字典数据"""
|
||||
# 点击新增按钮
|
||||
await self.page.get_by_role('button', name='新增').first.click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
await dialog.get_by_role('textbox', name='数据标签').fill(label)
|
||||
await dialog.get_by_role('textbox', name='数据键值').fill(value)
|
||||
await dialog.get_by_role('spinbutton', name='显示排序').fill(sort)
|
||||
|
||||
# 选择回显样式
|
||||
# 点击下拉框触发器
|
||||
# el-select 的结构通常是 wrapper -> input (readonly)
|
||||
# 我们可以点击 label 为 "回显样式" 的 form-item 下的 select
|
||||
select_trigger = dialog.locator("label:has-text('回显样式') + div .el-select")
|
||||
if await select_trigger.count() == 0:
|
||||
# 尝试直接找 .el-select,可能布局不同
|
||||
select_trigger = dialog.locator('.el-select').filter(has_text='默认(default)').first
|
||||
|
||||
# 如果还是找不到,尝试点击那个默认值文本
|
||||
if await select_trigger.count() == 0:
|
||||
await dialog.get_by_text('默认(default)').click()
|
||||
else:
|
||||
await select_trigger.click()
|
||||
|
||||
# 选择选项 (选项在 body 的 el-popper 中)
|
||||
# 等待选项出现
|
||||
await self.page.get_by_role('option', name=list_class).click()
|
||||
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
await self.wait_for_selector("div:has-text('新增成功')", timeout=10000)
|
||||
|
||||
async def edit_dict_data(self, label: str, remark: str) -> None:
|
||||
"""修改字典数据"""
|
||||
# 点击修改按钮
|
||||
row = self.page.locator('tbody tr').filter(has_text=label).first
|
||||
await row.get_by_role('button', name='修改').click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
await dialog.get_by_role('textbox', name='备注').fill(remark)
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
await self.wait_for_selector("div:has-text('修改成功')", timeout=10000)
|
||||
|
||||
async def delete_dict_data(self, label: str) -> None:
|
||||
"""删除字典数据"""
|
||||
# 点击删除按钮
|
||||
row = self.page.locator('tbody tr').filter(has_text=label).first
|
||||
await row.get_by_role('button', name='删除').click()
|
||||
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector("div:has-text('删除成功')", timeout=10000)
|
||||
|
||||
async def delete_dict_type(self, dict_name: str) -> None:
|
||||
"""删除字典类型"""
|
||||
await self.search_dict_type(dict_name, '')
|
||||
|
||||
# 点击删除按钮
|
||||
row = self.page.locator('tbody tr').filter(has_text=dict_name).first
|
||||
await row.get_by_role('button', name='删除').click()
|
||||
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector("div:has-text('删除成功')", timeout=10000)
|
||||
|
||||
async def test_dict_crud_operations(self) -> None:
|
||||
"""测试字典管理全流程"""
|
||||
data = self.generate_dict_data()
|
||||
|
||||
# 1. 进入字典管理
|
||||
await self.goto_page(Config.frontend_url + '/system/dict')
|
||||
# 等待页面加载
|
||||
await self.wait_for_selector('text=字典名称') # 页面标题或其他标识
|
||||
|
||||
# 2. 创建字典类型
|
||||
await self.create_dict_type(data['dict_name'], data['dict_type'])
|
||||
|
||||
# 3. 搜索并修改
|
||||
await self.edit_dict_type(data['dict_name'], data['remark'])
|
||||
|
||||
# 4. 管理字典数据
|
||||
await self.manage_dict_data(data['dict_type'], data)
|
||||
|
||||
# 5. 删除字典类型
|
||||
await self.delete_dict_type(data['dict_name'])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dict_management_page() -> None:
|
||||
"""测试字典管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = DictManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_dict_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,138 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class LogManagementTest(BasePageTest):
|
||||
"""日志管理测试类"""
|
||||
|
||||
async def test_operlog_operations(self) -> None:
|
||||
"""测试操作日志查询、删除和清空"""
|
||||
# 1. 导航到操作日志页面
|
||||
# 直接访问正确的路由
|
||||
await self.page.goto(Config.frontend_url + '/system/log/operlog')
|
||||
await self.page.wait_for_load_state('networkidle')
|
||||
|
||||
try:
|
||||
await self.page.wait_for_selector('.app-container', timeout=10000)
|
||||
except Exception:
|
||||
# 如果首次加载失败,尝试刷新
|
||||
await self.page.reload()
|
||||
await self.page.wait_for_selector('.app-container', timeout=10000)
|
||||
|
||||
# 2. 查询操作
|
||||
# 输入操作人员
|
||||
await self.page.locator('div.el-form-item', has_text='操作人员').locator('input').fill('admin')
|
||||
# 点击搜索按钮
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
await self.page.wait_for_timeout(1000) # 等待搜索结果
|
||||
# 点击重置按钮
|
||||
await self.page.get_by_role('button', name='重置').click()
|
||||
await self.page.wait_for_timeout(1000) # 等待重置结果
|
||||
|
||||
# 3. 删除操作
|
||||
# 检查是否有数据,如果有则进行删除测试
|
||||
rows = self.page.locator('.el-table__body tr')
|
||||
count = await rows.count()
|
||||
if count > 0:
|
||||
# 选中第一条
|
||||
await rows.first.locator('.el-checkbox').click()
|
||||
# 点击删除
|
||||
await self.page.get_by_role('button', name='删除').click()
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
# 等待删除完成提示
|
||||
try:
|
||||
await self.page.wait_for_selector('text=删除成功', timeout=10000)
|
||||
except Exception:
|
||||
pass # 可能是 toast 提示,稍纵即逝
|
||||
|
||||
# 4. 清空操作
|
||||
# 点击清空
|
||||
await self.page.get_by_role('button', name='清空').click()
|
||||
# 确认清空
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
# 5. 验证清空后还有一条"清空"的操作日志
|
||||
await self.page.wait_for_timeout(1000) # 等待清空操作完成并刷新
|
||||
await self.page.get_by_role('button', name='搜索').click() # 刷新列表
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
# 验证列表不为空 (因为清空操作本身会产生一条日志)
|
||||
rows_after_clean = self.page.locator('.el-table__body tr')
|
||||
count_after_clean = await rows_after_clean.count()
|
||||
assert count_after_clean > 0, '操作日志清空后应该至少有一条记录(清空操作本身)'
|
||||
|
||||
# 可以在这里进一步验证第一条记录的类型是否为清空,但题目只要求不是空列表
|
||||
|
||||
async def test_logininfor_operations(self) -> None:
|
||||
"""测试登录日志查询、删除和清空"""
|
||||
# 1. 导航到登录日志页面
|
||||
await self.page.goto(Config.frontend_url + '/system/log/logininfor')
|
||||
await self.page.wait_for_load_state('networkidle')
|
||||
|
||||
try:
|
||||
await self.page.wait_for_selector('.app-container', timeout=10000)
|
||||
except Exception:
|
||||
# 如果失败,尝试刷新
|
||||
await self.page.reload()
|
||||
await self.page.wait_for_selector('.app-container', timeout=10000)
|
||||
|
||||
# 2. 查询操作
|
||||
# 输入用户名称
|
||||
await self.page.locator('div.el-form-item', has_text='用户名称').locator('input').fill('admin')
|
||||
# 点击搜索
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
# 点击重置
|
||||
await self.page.get_by_role('button', name='重置').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
# 3. 删除操作
|
||||
# 检查是否有数据
|
||||
rows = self.page.locator('.el-table__body tr')
|
||||
count = await rows.count()
|
||||
if count > 0:
|
||||
# 选中第一条
|
||||
await rows.first.locator('.el-checkbox').click()
|
||||
# 点击删除
|
||||
await self.page.get_by_role('button', name='删除').click()
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
# 等待删除成功
|
||||
try:
|
||||
await self.page.wait_for_selector('text=删除成功', timeout=10000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 4. 清空操作
|
||||
# 点击清空
|
||||
await self.page.get_by_role('button', name='清空').click()
|
||||
# 确认清空
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
# 5. 验证清空后列表为空
|
||||
await self.page.wait_for_timeout(1000)
|
||||
# 检查是否显示"暂无数据"
|
||||
no_data = await self.page.get_by_text('暂无数据').is_visible()
|
||||
if not no_data:
|
||||
# 再次检查行数
|
||||
count_final = await self.page.locator('.el-table__body tr').count()
|
||||
assert count_final == 0, '登录日志清空后应该没有数据'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_log_management_page() -> None:
|
||||
"""测试日志管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = LogManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
# 运行操作日志测试
|
||||
await test_instance.test_operlog_operations()
|
||||
# 运行登录日志测试
|
||||
await test_instance.test_logininfor_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,145 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class MenuManagementTest(BasePageTest):
|
||||
"""菜单管理测试"""
|
||||
|
||||
def generate_menu_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = datetime.now().strftime('%H%M%S')
|
||||
return {
|
||||
'dir_name': f'测试目录_{timestamp}',
|
||||
'menu_name': f'测试菜单_{timestamp}',
|
||||
'path_dir': f'test_dir_{timestamp}',
|
||||
'path_menu': f'test_menu_{timestamp}',
|
||||
'path_menu_new': f'test_menu_new_{timestamp}',
|
||||
'order_num': '10',
|
||||
}
|
||||
|
||||
async def create_directory(self, menu_name: str, path: str, order_num: str) -> None:
|
||||
"""创建目录"""
|
||||
# 点击顶部新增按钮
|
||||
await self.page.get_by_role('button', name='新增').first.click()
|
||||
|
||||
# 等待对话框出现
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写表单
|
||||
await dialog.get_by_role('spinbutton', name='显示排序').fill(order_num)
|
||||
await dialog.get_by_role('textbox', name='菜单名称').fill(menu_name)
|
||||
await dialog.get_by_role('textbox', name='路由地址').fill(path)
|
||||
|
||||
# 提交
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 验证成功提示
|
||||
await self.wait_for_selector("div:has-text('新增成功')", timeout=10000)
|
||||
|
||||
async def search_menu(self, menu_name: str) -> None:
|
||||
"""搜索菜单"""
|
||||
# 填写搜索条件
|
||||
# 使用 first 匹配搜索表单(通常在顶部),避免匹配到隐藏对话框中的同名字段
|
||||
await self.page.locator('form').first.get_by_role('textbox', name='菜单名称').fill(menu_name)
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
|
||||
# 等待表格刷新(简单等待)
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def create_sub_menu(self, parent_name: str, menu_name: str, path: str, order_num: str) -> None:
|
||||
"""在指定目录下创建子菜单"""
|
||||
# 找到父级菜单行
|
||||
row = self.page.locator('tbody tr').filter(has_text=parent_name).first
|
||||
|
||||
# 点击行内新增按钮
|
||||
await row.get_by_role('button', name='新增').click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 选择菜单类型为"菜单"
|
||||
await dialog.locator('label').filter(has_text='菜单').first.click()
|
||||
|
||||
# 填写表单
|
||||
await dialog.get_by_role('spinbutton', name='显示排序').fill(order_num)
|
||||
await dialog.get_by_role('textbox', name='菜单名称').fill(menu_name)
|
||||
await dialog.get_by_role('textbox', name='路由地址').fill(path)
|
||||
|
||||
# 提交
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 验证成功
|
||||
await self.wait_for_selector("div:has-text('新增成功')", timeout=10000)
|
||||
|
||||
async def edit_sub_menu(self, menu_name: str, new_path: str) -> None:
|
||||
"""修改子菜单"""
|
||||
# 搜索子菜单
|
||||
await self.search_menu(menu_name)
|
||||
|
||||
row = self.page.locator('tbody tr').filter(has_text=menu_name).first
|
||||
await row.get_by_role('button', name='修改').click()
|
||||
|
||||
await self.wait_for_selector('.el-dialog', timeout=5000)
|
||||
dialog = self.page.locator('.el-dialog')
|
||||
|
||||
await dialog.get_by_role('textbox', name='路由地址').fill(new_path)
|
||||
await self.page.get_by_role('button', name='确 定').click()
|
||||
|
||||
await self.wait_for_selector("div:has-text('修改成功')", timeout=10000)
|
||||
|
||||
async def delete_menus(self, menu_names: list[str]) -> None:
|
||||
"""删除菜单(列表)"""
|
||||
for name in menu_names:
|
||||
await self.search_menu(name)
|
||||
row = self.page.locator('tbody tr').filter(has_text=name).first
|
||||
|
||||
# 点击删除
|
||||
await row.get_by_role('button', name='删除').click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
await self.wait_for_selector("div:has-text('删除成功')", timeout=10000)
|
||||
|
||||
async def test_menu_crud_operations(self) -> None:
|
||||
"""测试菜单增删改查流程"""
|
||||
data = self.generate_menu_data()
|
||||
|
||||
# 1. 进入菜单管理页面
|
||||
await self.goto_page(Config.frontend_url + '/system/menu')
|
||||
await self.wait_for_page_title('菜单管理', timeout=10000)
|
||||
|
||||
# 2. 新增目录
|
||||
await self.create_directory(data['dir_name'], data['path_dir'], data['order_num'])
|
||||
|
||||
# 3. 搜索目录并新增子菜单
|
||||
await self.search_menu(data['dir_name'])
|
||||
await self.create_sub_menu(data['dir_name'], data['menu_name'], data['path_menu'], '1')
|
||||
|
||||
# 4. 修改子菜单
|
||||
await self.edit_sub_menu(data['menu_name'], data['path_menu_new'])
|
||||
|
||||
# 5. 删除子菜单
|
||||
await self.delete_menus([data['menu_name']])
|
||||
|
||||
# 6. 删除目录
|
||||
await self.delete_menus([data['dir_name']])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_menu_management_page() -> None:
|
||||
"""测试菜单管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = MenuManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_menu_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,165 @@
|
||||
import re
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class NoticeManagementTest(BasePageTest):
|
||||
"""通知公告管理测试类"""
|
||||
|
||||
def generate_notice_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = int(time.time())
|
||||
return {
|
||||
'notice_title': f'test_notice_{timestamp}',
|
||||
'updated_title': f'updated_notice_{timestamp}',
|
||||
}
|
||||
|
||||
async def create_notice(self, notice_title: str, notice_type: int = 1, notice_content: str = '测试内容') -> None:
|
||||
"""创建通知公告"""
|
||||
# 点击新增按钮
|
||||
await self.page.get_by_role('button', name='新增').first.click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写公告信息
|
||||
await dialog.get_by_role('textbox', name='公告标题').fill(notice_title)
|
||||
|
||||
# 选择公告类型
|
||||
# 根据codegen脚本,点击下拉框触发器
|
||||
await self.page.locator('div').filter(has_text=re.compile(r'^请选择$')).nth(4).click()
|
||||
# 选择对应类型选项,1为通知,2为公告
|
||||
if notice_type == 1:
|
||||
await self.page.get_by_role('option', name='通知').click()
|
||||
else:
|
||||
await self.page.get_by_role('option', name='公告').click()
|
||||
|
||||
# 填写公告内容
|
||||
await self.page.locator('.ql-container').click()
|
||||
await self.page.locator('.ql-editor').fill(notice_content)
|
||||
|
||||
# 等待一段时间确保输入完成
|
||||
await self.page.wait_for_timeout(500)
|
||||
|
||||
# 点击确认按钮
|
||||
await self.page.get_by_role('button', name='确 定').first.click()
|
||||
|
||||
# 等待一段时间确保请求发送
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
# 等待新增成功提示
|
||||
await self.wait_for_selector('div:has-text("新增成功")', timeout=10000)
|
||||
|
||||
async def search_notice(self, notice_title: str) -> None:
|
||||
"""搜索通知公告"""
|
||||
# 在查询表单中输入公告标题进行查询
|
||||
await self.page.get_by_role('textbox', name='公告标题').fill(notice_title)
|
||||
|
||||
# 点击搜索按钮
|
||||
await self.page.get_by_role('button', name='搜索').first.click()
|
||||
|
||||
# 等待搜索结果加载
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def edit_notice(self, updated_title: str, updated_content: str = '更新的测试内容') -> None:
|
||||
"""编辑通知公告"""
|
||||
# 点击编辑按钮
|
||||
await self.page.locator('tbody').get_by_role('button', name='修改').nth(0).click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 修改公告标题
|
||||
await dialog.get_by_role('textbox', name='公告标题').fill(updated_title)
|
||||
|
||||
# 修改公告内容
|
||||
await self.page.locator('.ql-editor').fill(updated_content)
|
||||
|
||||
# 点击确认按钮
|
||||
await self.page.get_by_role('button', name='确 定').first.click()
|
||||
|
||||
# 等待编辑成功提示
|
||||
await self.wait_for_selector('div:has-text("修改成功")', timeout=10000)
|
||||
|
||||
async def delete_notice(self) -> None:
|
||||
"""删除通知公告"""
|
||||
# 点击删除按钮
|
||||
await self.page.locator('tbody').get_by_role('button', name='删除').nth(0).click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').first.click()
|
||||
|
||||
# 等待删除成功提示
|
||||
await self.wait_for_selector('div:has-text("删除成功")', timeout=10000)
|
||||
|
||||
# 重置查询表单,确保能看到所有公告
|
||||
await self.page.get_by_role('button', name='重置').click()
|
||||
|
||||
# 等待列表刷新
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def test_notice_crud_operations(self) -> None:
|
||||
"""测试通知公告的增删查改功能"""
|
||||
# 访问通知公告页面
|
||||
await self.goto_page(Config.frontend_url + '/system/notice')
|
||||
|
||||
# 等待页面加载完成
|
||||
await self.wait_for_page_title('通知公告', timeout=10000)
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
|
||||
# 记录初始公告数量
|
||||
initial_notice_count = await self.get_table_total_rows()
|
||||
|
||||
# 生成测试数据
|
||||
data = self.generate_notice_data()
|
||||
|
||||
# 创建公告
|
||||
await self.create_notice(data['notice_title'])
|
||||
|
||||
# 验证公告已添加到列表中
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
new_notice_count = await self.get_table_total_rows()
|
||||
assert new_notice_count > initial_notice_count, '新增公告后数量应该增加'
|
||||
|
||||
# 搜索公告
|
||||
await self.search_notice(data['notice_title'])
|
||||
|
||||
# 验证搜索结果中包含新增的公告
|
||||
search_result_count = await self.get_table_total_rows()
|
||||
assert search_result_count >= 1, '搜索结果应该至少包含一个公告'
|
||||
|
||||
# 编辑公告
|
||||
await self.edit_notice(data['updated_title'])
|
||||
|
||||
# 验证编辑成功
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
|
||||
# 重新搜索以验证修改结果
|
||||
await self.search_notice(data['updated_title'])
|
||||
|
||||
# 删除公告
|
||||
await self.delete_notice()
|
||||
|
||||
# 验证公告已从列表中删除
|
||||
await self.page.wait_for_timeout(1000) # 等待列表刷新
|
||||
final_notice_count = await self.get_table_total_rows()
|
||||
assert final_notice_count == initial_notice_count, '删除公告后数量应该恢复到初始值'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notice_management_page() -> None:
|
||||
"""测试通知公告页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = NoticeManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_notice_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,136 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class PostManagementTest(BasePageTest):
|
||||
"""岗位管理测试类"""
|
||||
|
||||
def generate_post_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = datetime.now().strftime('%H%M%S')
|
||||
return {
|
||||
'post_name': f'测试岗位_{timestamp}',
|
||||
'post_code': f'test_{timestamp}',
|
||||
'post_sort': '10',
|
||||
'remark': f'测试备注_{timestamp}',
|
||||
}
|
||||
|
||||
async def test_post_crud_operations(self) -> None:
|
||||
"""测试岗位增删改查流程"""
|
||||
# 1. 导航到岗位管理页面
|
||||
await self.navigate_to_post_management()
|
||||
|
||||
# 生成测试数据
|
||||
data = self.generate_post_data()
|
||||
|
||||
# 2. 新增岗位
|
||||
await self.create_post(data['post_name'], data['post_code'], data['post_sort'])
|
||||
|
||||
# 3. 搜索岗位
|
||||
await self.search_post(data['post_name'], data['post_code'])
|
||||
|
||||
# 4. 修改岗位
|
||||
await self.edit_post(data['post_name'], data['remark'])
|
||||
|
||||
# 5. 删除岗位
|
||||
await self.delete_post(data['post_name'])
|
||||
|
||||
async def navigate_to_post_management(self) -> None:
|
||||
"""导航到岗位管理"""
|
||||
# 直接导航到岗位管理页面
|
||||
await self.goto_page(Config.frontend_url + '/system/post')
|
||||
# 等待页面加载
|
||||
await self.wait_for_selector('.app-container')
|
||||
|
||||
async def create_post(self, post_name: str, post_code: str, post_sort: str) -> None:
|
||||
"""新增岗位"""
|
||||
await self.page.get_by_role('button', name='新增').click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写表单
|
||||
# 注意:Element Plus 表单必填项可能有 * 号,Label 匹配可能需要注意
|
||||
await dialog.get_by_role('textbox', name='岗位名称').fill(post_name)
|
||||
await dialog.get_by_role('textbox', name='岗位编码').fill(post_code)
|
||||
|
||||
# 岗位顺序是 input-number
|
||||
# script.py 使用 spinbutton
|
||||
await dialog.get_by_role('spinbutton', name='岗位顺序').fill(post_sort)
|
||||
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector("div:has-text('新增成功')", timeout=10000)
|
||||
|
||||
async def search_post(self, post_name: str, post_code: str) -> None:
|
||||
"""搜索岗位"""
|
||||
# 填写搜索表单
|
||||
# 限定在搜索区域,防止定位到弹窗或其他地方(虽然此时没有弹窗)
|
||||
search_form = self.page.locator('.el-form').first
|
||||
|
||||
await search_form.get_by_role('textbox', name='岗位编码').fill(post_code)
|
||||
await search_form.get_by_role('textbox', name='岗位名称').fill(post_name)
|
||||
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
|
||||
# 等待表格加载结果
|
||||
# 验证表格中包含刚创建的岗位
|
||||
await self.wait_for_selector(f"tbody tr:has-text('{post_name}')", timeout=10000)
|
||||
|
||||
async def edit_post(self, post_name: str, remark: str) -> None:
|
||||
"""修改岗位"""
|
||||
# 确保当前显示的是我们要修改的岗位
|
||||
await self.search_post(post_name, '')
|
||||
|
||||
row = self.page.locator('tbody tr').first
|
||||
# 点击修改按钮
|
||||
await row.get_by_role('button', name='修改').click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 修改备注
|
||||
await dialog.get_by_role('textbox', name='备注').fill(remark)
|
||||
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector("div:has-text('修改成功')", timeout=10000)
|
||||
|
||||
async def delete_post(self, post_name: str) -> None:
|
||||
"""删除岗位"""
|
||||
# 确保当前显示的是我们要删除的岗位
|
||||
search_form = self.page.locator('.el-form').first
|
||||
await search_form.get_by_role('textbox', name='岗位编码').clear()
|
||||
await search_form.get_by_role('textbox', name='岗位名称').fill(post_name)
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
|
||||
await self.wait_for_selector(f"tbody tr:has-text('{post_name}')", timeout=10000)
|
||||
|
||||
row = self.page.locator('tbody tr').first
|
||||
# 点击删除按钮
|
||||
await row.get_by_role('button', name='删除').click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector("div:has-text('删除成功')", timeout=10000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_management_page() -> None:
|
||||
"""测试岗位管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = PostManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_post_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,148 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class RoleManagementTest(BasePageTest):
|
||||
"""角色管理测试类"""
|
||||
|
||||
def generate_role_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = int(time.time())
|
||||
return {
|
||||
'role_name': f'test_role_{timestamp}',
|
||||
'role_key': f'test_{timestamp}',
|
||||
}
|
||||
|
||||
async def create_role(self, role_name: str, role_key: str, role_sort: int = 3) -> None:
|
||||
"""创建角色"""
|
||||
# 点击新增按钮
|
||||
await self.page.get_by_role('button', name='新增').click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写信息
|
||||
await dialog.get_by_role('textbox', name='角色名称').fill(role_name)
|
||||
await dialog.get_by_role('textbox', name='权限字符').fill(role_key)
|
||||
await dialog.get_by_role('spinbutton', name='角色顺序').fill(str(role_sort))
|
||||
|
||||
# 选择菜单权限 (点击第一个复选框)
|
||||
await self.page.locator('.el-tree-node__content .el-checkbox').first.click()
|
||||
|
||||
# 确定
|
||||
await self.page.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector('div:has-text("新增成功")', timeout=10000)
|
||||
|
||||
async def search_role(self, role_name: str, role_key: str) -> None:
|
||||
"""搜索角色"""
|
||||
search_form = self.page.locator('form').first
|
||||
await search_form.get_by_placeholder('请输入角色名称').fill(role_name)
|
||||
await search_form.get_by_placeholder('请输入权限字符').fill(role_key)
|
||||
await search_form.get_by_role('button', name='搜索').click()
|
||||
|
||||
# 等待加载
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def edit_role(self, new_remark: str) -> None:
|
||||
"""编辑角色"""
|
||||
# 点击修改按钮 (第一行)
|
||||
# 按钮没有文字,只有图标,所以使用 nth(0)
|
||||
row = self.page.locator('tbody tr').first
|
||||
await row.get_by_role('button').nth(0).click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 修改备注
|
||||
await dialog.get_by_role('textbox', name='备注').fill(new_remark)
|
||||
|
||||
# 确定
|
||||
await self.page.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector('div:has-text("修改成功")', timeout=10000)
|
||||
|
||||
async def change_role_status(self) -> None:
|
||||
"""修改角色状态"""
|
||||
# 点击开关 (第一行)
|
||||
# 使用 .el-switch__core 或者 role=switch
|
||||
# script.py uses .el-switch__action, let's try .el-switch
|
||||
switch = self.page.locator('.el-switch').first
|
||||
await switch.click()
|
||||
|
||||
# 确认对话框
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector('div:has-text("成功")', timeout=5000)
|
||||
|
||||
# 再次点击恢复
|
||||
await self.page.wait_for_timeout(1000)
|
||||
await switch.click()
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector('div:has-text("成功")', timeout=5000)
|
||||
|
||||
async def delete_role(self) -> None:
|
||||
"""删除角色"""
|
||||
# 点击删除按钮 (第一行第二个按钮)
|
||||
row = self.page.locator('tbody tr').first
|
||||
await row.get_by_role('button').nth(1).click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector('div:has-text("删除成功")', timeout=10000)
|
||||
|
||||
# 重置搜索
|
||||
await self.page.get_by_role('button', name='重置').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def test_role_crud_operations(self) -> None:
|
||||
"""测试角色管理CRUD"""
|
||||
# 访问页面
|
||||
await self.goto_page(Config.frontend_url + '/system/role')
|
||||
await self.wait_for_page_title('角色管理', timeout=10000)
|
||||
|
||||
# 生成测试数据
|
||||
data = self.generate_role_data()
|
||||
|
||||
# 1. 新增
|
||||
await self.create_role(data['role_name'], data['role_key'])
|
||||
|
||||
# 验证新增结果
|
||||
await self.search_role(data['role_name'], data['role_key'])
|
||||
rows = await self.get_table_total_rows()
|
||||
assert rows >= 1, '新增后搜索应有结果'
|
||||
|
||||
# 2. 修改状态
|
||||
await self.change_role_status()
|
||||
|
||||
# 3. 编辑
|
||||
await self.edit_role('Updated remark')
|
||||
|
||||
# 4. 删除
|
||||
await self.delete_role()
|
||||
|
||||
# 验证删除结果
|
||||
await self.search_role(data['role_name'], data['role_key'])
|
||||
rows_after = await self.get_table_total_rows()
|
||||
assert rows_after == 0, '删除后搜索应无结果'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_management_page() -> None:
|
||||
"""测试角色管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = RoleManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_role_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,170 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class UserManagementTest(BasePageTest):
|
||||
"""用户管理测试类"""
|
||||
|
||||
def generate_user_data(self) -> dict:
|
||||
"""生成测试数据"""
|
||||
timestamp = int(time.time())
|
||||
return {
|
||||
'user_name': f'test_{timestamp}',
|
||||
'nick_name': f'测试用户_{timestamp}',
|
||||
'phone': '13888888888',
|
||||
}
|
||||
|
||||
async def create_user(self, user_name: str, nick_name: str, phone: str) -> None:
|
||||
"""创建用户"""
|
||||
# 点击新增按钮
|
||||
await self.page.get_by_role('button', name='新增').click()
|
||||
|
||||
# 等待对话框
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 填写基本信息
|
||||
await dialog.get_by_role('textbox', name='用户昵称').fill(nick_name)
|
||||
|
||||
# 归属部门
|
||||
# 使用 label 定位父级 form-item,再点击内部的 wrapper
|
||||
await dialog.locator('div.el-form-item').filter(has_text='归属部门').locator('.el-select__wrapper').click()
|
||||
# 选择 "集团总公司" (根节点)
|
||||
# 使用 .el-popper 定位下拉框中的内容
|
||||
await self.page.locator('.el-popper:visible').get_by_text('集团总公司').click()
|
||||
|
||||
await dialog.get_by_role('textbox', name='用户名称').fill(user_name)
|
||||
# 手机号码在搜索框也有,所以必须限定在 dialog 内
|
||||
# 注意:有些 label 可能不带 *,或者带 * 但 get_by_role 需要准确匹配
|
||||
# 这里使用 get_by_placeholder 或者 filter 来定位更稳妥,或者直接 scope 到 dialog
|
||||
await dialog.locator('div.el-form-item').filter(has_text='手机号码').get_by_role('textbox').fill(phone)
|
||||
await dialog.locator('div.el-form-item').filter(has_text='邮箱').get_by_role('textbox').fill('test@example.com')
|
||||
await dialog.locator('div.el-form-item').filter(has_text='用户密码').get_by_role('textbox').fill('123456')
|
||||
|
||||
# 用户性别
|
||||
# 定位 label 为 "用户性别" 的父级 form-item,然后找里面的 "请选择" 文本
|
||||
await dialog.locator('div.el-form-item').filter(has_text='用户性别').get_by_text('请选择').click()
|
||||
await self.page.get_by_role('option', name='男').click()
|
||||
|
||||
# 岗位
|
||||
await dialog.locator('div.el-form-item').filter(has_text='岗位').get_by_text('请选择').click()
|
||||
await self.page.get_by_role('option', name='董事长').click()
|
||||
|
||||
# 角色
|
||||
await dialog.locator('div.el-form-item').filter(has_text='角色').get_by_text('请选择').click()
|
||||
await self.page.get_by_role('option', name='普通角色').click()
|
||||
|
||||
await dialog.locator('div.el-form-item').filter(has_text='备注').get_by_role('textbox').fill('测试用户')
|
||||
|
||||
# 点击确定
|
||||
await self.page.get_by_role('button', name='确 定').click()
|
||||
# 等待成功消息
|
||||
await self.wait_for_selector("div:has-text('成功')", timeout=10000)
|
||||
|
||||
async def search_user(self, user_name: str) -> None:
|
||||
"""搜索用户"""
|
||||
search_form = self.page.locator('form').first
|
||||
await search_form.get_by_role('textbox', name='用户名称').fill(user_name)
|
||||
await search_form.get_by_role('button', name='搜索').click()
|
||||
|
||||
# 等待加载
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def edit_user(self) -> None:
|
||||
"""编辑用户"""
|
||||
# 点击修改按钮 (第一行)
|
||||
row = self.page.locator('tbody tr').first
|
||||
await row.get_by_role('button').nth(0).click()
|
||||
|
||||
dialog = self.page.get_by_role('dialog')
|
||||
await dialog.wait_for()
|
||||
|
||||
# 修改性别为女
|
||||
await dialog.locator('div.el-form-item').filter(has_text='用户性别').locator('.el-select').click()
|
||||
await self.page.get_by_role('option', name='女').click()
|
||||
|
||||
# 确定
|
||||
await self.page.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector('div:has-text("修改成功")', timeout=10000)
|
||||
|
||||
async def change_user_status(self) -> None:
|
||||
"""修改用户状态"""
|
||||
# 点击开关 (第一行)
|
||||
switch = self.page.locator('.el-switch').first
|
||||
await switch.click()
|
||||
|
||||
# 确认对话框
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector('div:has-text("成功")', timeout=5000)
|
||||
|
||||
# 再次点击恢复
|
||||
await self.page.wait_for_timeout(1000)
|
||||
await switch.click()
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
await self.wait_for_selector('div:has-text("成功")', timeout=5000)
|
||||
|
||||
async def delete_user(self) -> None:
|
||||
"""删除用户"""
|
||||
# 点击删除按钮 (第一行第二个按钮)
|
||||
row = self.page.locator('tbody tr').first
|
||||
await row.get_by_role('button').nth(1).click()
|
||||
|
||||
# 确认删除
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
# 等待成功提示
|
||||
await self.wait_for_selector('div:has-text("删除成功")', timeout=10000)
|
||||
|
||||
# 重置搜索
|
||||
await self.page.get_by_role('button', name='重置').click()
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
async def test_user_crud_operations(self) -> None:
|
||||
"""测试用户管理增删查改"""
|
||||
# 访问页面
|
||||
await self.goto_page(Config.frontend_url + '/system/user')
|
||||
await self.wait_for_page_title('用户管理', timeout=10000)
|
||||
|
||||
# 生成测试数据
|
||||
data = self.generate_user_data()
|
||||
|
||||
# 1. 新增
|
||||
await self.create_user(data['user_name'], data['nick_name'], data['phone'])
|
||||
|
||||
# 验证新增结果
|
||||
await self.search_user(data['user_name'])
|
||||
rows = await self.get_table_total_rows()
|
||||
assert rows >= 1, '新增后搜索应有结果'
|
||||
|
||||
# 2. 修改状态
|
||||
await self.change_user_status()
|
||||
|
||||
# 3. 编辑
|
||||
await self.edit_user()
|
||||
|
||||
# 4. 删除
|
||||
await self.delete_user()
|
||||
|
||||
# 验证删除结果
|
||||
await self.search_user(data['user_name'])
|
||||
rows_after = await self.get_table_total_rows()
|
||||
assert rows_after == 0, '删除后搜索应无结果'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_management_page() -> None:
|
||||
"""测试用户管理页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = UserManagementTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_user_crud_operations()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,202 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.config import Config
|
||||
from common.login_helper import LoginHelper
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_page_loads() -> None:
|
||||
"""测试登录页面是否能正常加载"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page()
|
||||
|
||||
# 访问登录页面
|
||||
await page.goto(Config.frontend_url + '/login')
|
||||
|
||||
# 检查页面标题或特定元素是否存在
|
||||
title = await page.title()
|
||||
assert 'vfadmin' in title
|
||||
|
||||
# 检查登录表单元素是否存在
|
||||
username_input = await page.query_selector('input[placeholder="账号"]')
|
||||
password_input = await page.query_selector('input[placeholder="密码"]')
|
||||
|
||||
assert username_input is not None
|
||||
assert password_input is not None
|
||||
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_captcha_generation() -> None:
|
||||
"""测试验证码是否正常生成(在测试环境中,验证码已禁用,但仍应能访问登录页面)"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page()
|
||||
|
||||
# 访问登录页面
|
||||
await page.goto(Config.frontend_url + '/login')
|
||||
|
||||
# 在测试环境中验证码已禁用,但仍应能访问登录页面
|
||||
title = await page.title()
|
||||
assert 'vfadmin' in title
|
||||
|
||||
await browser.close()
|
||||
|
||||
|
||||
def test_login_without_captcha() -> None:
|
||||
"""测试在禁用验证码的情况下登录流程"""
|
||||
helper = LoginHelper()
|
||||
|
||||
# 尝试登录(测试环境中验证码已禁用)
|
||||
token = helper.login(username='admin', password='admin123')
|
||||
|
||||
# 验证登录结果
|
||||
assert token is not None, '登录应该成功'
|
||||
assert len(token) > 0, '应该返回有效的token'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_flow_with_playwright() -> None:
|
||||
"""使用Playwright测试完整的登录流程(测试环境中验证码已禁用)"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page()
|
||||
|
||||
# 访问登录页面
|
||||
await page.goto(Config.frontend_url + '/login')
|
||||
|
||||
# 等待页面加载
|
||||
await page.wait_for_selector('input[placeholder="账号"]')
|
||||
|
||||
# 输入用户名和密码
|
||||
await page.fill('input[placeholder="账号"]', 'admin')
|
||||
await page.fill('input[placeholder="密码"]', 'admin123')
|
||||
|
||||
# 在测试环境中验证码已禁用,所以不需要处理验证码
|
||||
# 直接点击登录按钮
|
||||
# 尝试多种可能的选择器
|
||||
try:
|
||||
await page.click('button:has-text("登录")')
|
||||
except Exception:
|
||||
try:
|
||||
await page.click('button[type="submit"]')
|
||||
except Exception:
|
||||
await page.click('button.el-button')
|
||||
|
||||
# 等待页面跳转或检查登录结果
|
||||
try:
|
||||
# 等待跳转到主页或检查登录成功消息
|
||||
await page.wait_for_url('**/index**', timeout=10000)
|
||||
success = True
|
||||
except Exception:
|
||||
# 检查是否有错误消息
|
||||
error_message = await page.query_selector('.el-message')
|
||||
if error_message:
|
||||
error_text = await error_message.text_content()
|
||||
print(f'登录失败: {error_text}')
|
||||
success = False
|
||||
else:
|
||||
success = False
|
||||
|
||||
assert success, '登录应该成功'
|
||||
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protected_routes_require_auth() -> None:
|
||||
"""测试受保护的路由需要认证"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page()
|
||||
|
||||
# 直接访问受保护的页面(如用户管理)
|
||||
await page.goto(Config.frontend_url + '/system/user')
|
||||
|
||||
# 检查是否被重定向到登录页面
|
||||
current_url = page.url
|
||||
|
||||
# 检查页面是否包含登录相关的元素(如账号密码输入框)
|
||||
try:
|
||||
has_username_input = await page.is_visible('input[placeholder*="账号" i], input[placeholder*="username" i]')
|
||||
except Exception:
|
||||
has_username_input = False
|
||||
|
||||
try:
|
||||
has_password_input = await page.is_visible('input[placeholder*="密码" i], input[placeholder*="password" i]')
|
||||
except Exception:
|
||||
has_password_input = False
|
||||
|
||||
try:
|
||||
has_login_button = await page.is_visible('button:has-text("登录"), button:has-text("Login"), .login-btn')
|
||||
except Exception:
|
||||
has_login_button = False
|
||||
|
||||
# 检查URL是否包含login路径
|
||||
is_redirected_to_login = '/login' in current_url.lower() or '/#/login' in current_url
|
||||
|
||||
# 断言:应该重定向到登录页面或页面包含登录相关元素
|
||||
assert is_redirected_to_login or has_username_input or has_password_input or has_login_button, (
|
||||
f'未登录用户访问受保护页面时,没有重定向到登录页。当前URL: {current_url}, 页面内容: {await page.content()}'
|
||||
)
|
||||
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticated_access() -> None:
|
||||
"""测试认证后的访问"""
|
||||
# 首先通过API登录获取token
|
||||
helper = LoginHelper()
|
||||
token = helper.login(username='admin', password='admin123')
|
||||
|
||||
assert token is not None, '登录应该成功'
|
||||
|
||||
# 使用Playwright测试带认证的访问
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
context = await browser.new_context(
|
||||
storage_state={ # 使用storage state来保持登录状态
|
||||
'cookies': [
|
||||
{
|
||||
'name': 'Admin-Token',
|
||||
'value': token,
|
||||
'domain': 'localhost',
|
||||
'path': '/',
|
||||
'httpOnly': False,
|
||||
'secure': False,
|
||||
'sameSite': 'Lax',
|
||||
}
|
||||
],
|
||||
'origins': [],
|
||||
}
|
||||
)
|
||||
page = await context.new_page()
|
||||
|
||||
# 访问受保护的页面
|
||||
await page.goto(Config.frontend_url + '/system/user')
|
||||
|
||||
# 检查是否能够访问受保护的页面(而不是被重定向到登录页)
|
||||
current_url = page.url
|
||||
assert '/login' not in current_url, '已登录用户应该能够访问受保护页面'
|
||||
|
||||
# 检查页面是否包含用户管理相关的元素
|
||||
try:
|
||||
# 等待页面加载
|
||||
await page.wait_for_selector('div:has-text("用户管理")', timeout=10000)
|
||||
has_user_management = True
|
||||
except Exception:
|
||||
has_user_management = False
|
||||
|
||||
# 或者检查是否有表格或其他用户管理组件
|
||||
table_element = await page.query_selector('el-table')
|
||||
if not has_user_management and table_element:
|
||||
has_user_management = True
|
||||
|
||||
assert has_user_management, '应该能够访问用户管理页面'
|
||||
|
||||
await context.close()
|
||||
await browser.close()
|
||||
@@ -0,0 +1,119 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from common.config import Config
|
||||
from common.login_helper import LoginHelper
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboard_page() -> None:
|
||||
"""测试仪表盘页面"""
|
||||
# 首先登录获取token
|
||||
helper = LoginHelper()
|
||||
token = helper.login(username='admin', password='admin123')
|
||||
assert token is not None, '登录应该成功'
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
context = await browser.new_context()
|
||||
# 设置认证token
|
||||
await context.add_cookies(
|
||||
[
|
||||
{
|
||||
'name': 'Admin-Token',
|
||||
'value': token,
|
||||
'domain': 'localhost',
|
||||
'path': '/',
|
||||
'httpOnly': False,
|
||||
'secure': False,
|
||||
}
|
||||
]
|
||||
)
|
||||
page = await context.new_page()
|
||||
|
||||
# 访问仪表盘页面
|
||||
await page.goto(Config.frontend_url + '/index')
|
||||
|
||||
# 检查页面是否包含仪表盘相关元素
|
||||
await page.wait_for_selector('div:has-text("首页")', timeout=10000)
|
||||
title = await page.inner_text('div:has-text("首页")')
|
||||
assert '首页' in title
|
||||
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_druid_page() -> None:
|
||||
"""测试数据监控页面"""
|
||||
# 首先登录获取token
|
||||
helper = LoginHelper()
|
||||
token = helper.login(username='admin', password='admin123')
|
||||
assert token is not None, '登录应该成功'
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
context = await browser.new_context()
|
||||
# 设置认证token
|
||||
await context.add_cookies(
|
||||
[
|
||||
{
|
||||
'name': 'Admin-Token',
|
||||
'value': token,
|
||||
'domain': 'localhost',
|
||||
'path': '/',
|
||||
'httpOnly': False,
|
||||
'secure': False,
|
||||
}
|
||||
]
|
||||
)
|
||||
page = await context.new_page()
|
||||
|
||||
# 访问数据库监控页面
|
||||
await page.goto(Config.frontend_url + '/monitor/druid')
|
||||
|
||||
# 检查页面是否包含缓存监控相关元素
|
||||
await page.wait_for_selector('div:has-text("数据监控")', timeout=10000)
|
||||
title = await page.inner_text('div:has-text("数据监控")')
|
||||
assert '数据监控' in title
|
||||
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_page() -> None:
|
||||
"""测试表单构建页面"""
|
||||
# 首先登录获取token
|
||||
helper = LoginHelper()
|
||||
token = helper.login(username='admin', password='admin123')
|
||||
assert token is not None, '登录应该成功'
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
context = await browser.new_context()
|
||||
# 设置认证token
|
||||
await context.add_cookies(
|
||||
[
|
||||
{
|
||||
'name': 'Admin-Token',
|
||||
'value': token,
|
||||
'domain': 'localhost',
|
||||
'path': '/',
|
||||
'httpOnly': False,
|
||||
'secure': False,
|
||||
}
|
||||
]
|
||||
)
|
||||
page = await context.new_page()
|
||||
|
||||
# 访问数据库监控页面
|
||||
await page.goto(Config.frontend_url + '/tool/build')
|
||||
|
||||
# 检查页面是否包含缓存监控相关元素
|
||||
await page.wait_for_selector('div:has-text("Form Generator")', timeout=10000)
|
||||
title = await page.inner_text('div:has-text("Form Generator")')
|
||||
assert '表单构建' in title
|
||||
|
||||
await context.close()
|
||||
await browser.close()
|
||||
@@ -0,0 +1,228 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright, expect
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class GenTableTest(BasePageTest):
|
||||
"""代码生成业务测试"""
|
||||
|
||||
async def test_gen_code_flow(self) -> None:
|
||||
"""测试代码生成全流程:导入 -> 编辑 -> 预览 -> 删除"""
|
||||
table_name = 'sys_post'
|
||||
|
||||
# 1. 导航到代码生成页面
|
||||
await self.navigate_to_gen_page()
|
||||
|
||||
# 确保环境清理 (如果存在 sys_post 则先删除)
|
||||
await self.ensure_table_not_exists(table_name)
|
||||
|
||||
# 2. 导入表
|
||||
await self.import_table(table_name)
|
||||
|
||||
# 3. 编辑表
|
||||
await self.edit_table(table_name, '测试生成')
|
||||
|
||||
# 4. 预览代码
|
||||
await self.preview_code(table_name)
|
||||
await self.page.wait_for_timeout(1000)
|
||||
|
||||
# 5. 删除表 (清理环境)
|
||||
await self.delete_table(table_name)
|
||||
|
||||
async def navigate_to_gen_page(self) -> None:
|
||||
"""导航到代码生成页面"""
|
||||
await self.goto_page(Config.frontend_url + '/tool/gen')
|
||||
await self.wait_for_selector('.app-container')
|
||||
|
||||
async def ensure_table_not_exists(self, table_name: str) -> None:
|
||||
"""确保表不存在 (如果存在则删除)"""
|
||||
await self.delete_table(table_name, verify=False)
|
||||
|
||||
async def search_table(self, table_name: str) -> None:
|
||||
"""搜索表"""
|
||||
# 确保在主搜索表单中搜索
|
||||
search_form = self.page.locator('.el-form').first
|
||||
await search_form.get_by_role('textbox', name='表名称').fill(table_name)
|
||||
await search_form.get_by_role('button', name='搜索').click()
|
||||
# 等待加载
|
||||
loading = self.page.locator('.el-loading-mask')
|
||||
if await loading.count() > 0:
|
||||
await expect(loading.first).to_be_hidden(timeout=10000)
|
||||
await self.page.wait_for_timeout(300)
|
||||
|
||||
async def import_table(self, table_name: str) -> None:
|
||||
"""导入表"""
|
||||
await self.page.get_by_role('button', name='导入').click()
|
||||
|
||||
dialog = self.page.locator("div[role='dialog'][aria-label='导入表']")
|
||||
await dialog.wait_for()
|
||||
|
||||
# 搜索要导入的表
|
||||
await dialog.get_by_placeholder('请输入表名称').fill(table_name)
|
||||
await dialog.get_by_role('button', name='搜索').click()
|
||||
|
||||
# 等待搜索结果
|
||||
await dialog.locator(f"tr:has-text('{table_name}')").wait_for()
|
||||
|
||||
# 选中行
|
||||
row = dialog.locator('tr').filter(has=self.page.get_by_text(table_name, exact=True))
|
||||
|
||||
# 点击复选框
|
||||
checkbox = row.locator('.el-checkbox')
|
||||
await checkbox.click()
|
||||
|
||||
# 验证已选中
|
||||
# Element Plus checkbox 选中时,最外层 label.el-checkbox 会有 is-checked 类
|
||||
await expect(checkbox).to_have_class(re.compile(r'is-checked'))
|
||||
|
||||
await self.page.wait_for_timeout(500)
|
||||
|
||||
await dialog.get_by_role('button', name='确 定').click()
|
||||
|
||||
# 检查是否有"请选择要导入的表"错误
|
||||
try:
|
||||
await expect(self.page.get_by_text('请选择要导入的表')).to_be_visible(timeout=2000)
|
||||
print("ERROR: Selection failed, '请选择要导入的表' appeared.")
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
# 等待一会,让弹窗自动关闭
|
||||
await self.page.wait_for_timeout(5000)
|
||||
|
||||
# 如果弹窗还在,尝试关闭它以免阻塞后续操作
|
||||
if await dialog.is_visible():
|
||||
print('WARNING: Import dialog still visible after timeout. Forcing close.')
|
||||
# Check for error messages
|
||||
if await self.page.locator('.el-message--error').count() > 0:
|
||||
msg = await self.page.locator('.el-message--error').all_inner_texts()
|
||||
print(f'ERROR MESSAGE: {msg}')
|
||||
|
||||
# 点击取消关闭弹窗
|
||||
await dialog.get_by_role('button', name='取 消').click()
|
||||
await expect(dialog).to_be_hidden()
|
||||
|
||||
# 手动刷新列表
|
||||
await self.page.get_by_role('button', name='搜索').click()
|
||||
|
||||
# 验证导入成功 (搜索并在列表中看到)
|
||||
# 使用 .app-container 限定在主页面表格,避免匹配到弹窗中的隐藏行
|
||||
await self.search_table(table_name)
|
||||
row = self.page.locator('.app-container .el-table__body-wrapper tbody tr').filter(
|
||||
has=self.page.get_by_text(table_name, exact=True)
|
||||
)
|
||||
for _i in range(5):
|
||||
try:
|
||||
await expect(row.first).to_be_visible(timeout=3000)
|
||||
break
|
||||
except AssertionError:
|
||||
await self.search_table(table_name)
|
||||
await expect(row.first).to_be_visible(timeout=5000)
|
||||
|
||||
async def edit_table(self, table_name: str, remark: str) -> None:
|
||||
"""编辑表"""
|
||||
await self.search_table(table_name)
|
||||
row = self.page.locator(f"tbody tr:has-text('{table_name}')")
|
||||
|
||||
# 点击编辑 (操作列第2个按钮,索引1)
|
||||
# 按钮顺序: 预览, 编辑, 删除, 同步, 生成
|
||||
await row.locator('button').nth(1).click()
|
||||
|
||||
# 等待编辑页面 (tab页)
|
||||
await self.page.wait_for_selector("div[role='tablist']")
|
||||
|
||||
# 修改基本信息 -> 表描述
|
||||
# 确保在基本信息 Tab
|
||||
await self.page.get_by_text('基本信息').click()
|
||||
await self.page.get_by_role('textbox', name='表描述').fill(remark)
|
||||
|
||||
# 提交
|
||||
await self.page.get_by_role('button', name='提交').click()
|
||||
|
||||
# 验证回到列表
|
||||
await self.wait_for_selector('.app-container')
|
||||
# 验证描述已更新
|
||||
await self.search_table(table_name)
|
||||
await self.wait_for_selector(f"tbody tr:has-text('{remark}')")
|
||||
|
||||
async def preview_code(self, table_name: str) -> None:
|
||||
"""预览代码"""
|
||||
await self.search_table(table_name)
|
||||
row = self.page.locator(f"tbody tr:has-text('{table_name}')")
|
||||
|
||||
# 点击预览 (操作列第1个按钮,索引0)
|
||||
await row.locator('button').nth(0).click()
|
||||
|
||||
# 等待预览弹窗
|
||||
dialog = self.page.locator("div[role='dialog'][aria-label='代码预览']")
|
||||
await dialog.wait_for()
|
||||
|
||||
# 验证存在代码内容
|
||||
# pre 可能有多个(多tab),只检查可见的
|
||||
await dialog.locator('pre:visible').first.wait_for()
|
||||
content = await dialog.locator('pre:visible').first.text_content()
|
||||
assert 'class' in content or 'import' in content or 'package' in content
|
||||
|
||||
# 关闭预览 (点击右上角关闭按钮)
|
||||
await dialog.locator('.el-dialog__headerbtn').click()
|
||||
|
||||
async def delete_table(self, table_name: str, verify: bool = True) -> None:
|
||||
"""删除表 (支持删除多条重复数据)"""
|
||||
# 循环删除直到不存在
|
||||
for _i in range(5):
|
||||
await self.search_table(table_name)
|
||||
# 使用 strict matching
|
||||
# 限制在 .app-container .el-table__body-wrapper 以避免固定列导致的重复 以及 避免匹配到弹窗中的行
|
||||
row = self.page.locator('.app-container .el-table__body-wrapper tbody tr').filter(
|
||||
has=self.page.get_by_text(table_name, exact=True)
|
||||
)
|
||||
|
||||
count = await row.count()
|
||||
|
||||
if count == 0:
|
||||
break
|
||||
|
||||
# 针对第一行操作
|
||||
# 使用 force=True 确保点击,防止遮挡
|
||||
btns = row.first.locator('button')
|
||||
await btns.nth(2).click(force=True)
|
||||
|
||||
# 处理确认弹窗
|
||||
await self.page.get_by_role('button', name='确定').click()
|
||||
|
||||
# 等待删除成功提示
|
||||
# 使用 specific selector 避免匹配到代码预览中的文本
|
||||
await expect(self.page.locator('.el-message__content').filter(has_text='删除成功')).to_be_visible(
|
||||
timeout=5000
|
||||
)
|
||||
|
||||
# 等待提示消失,防止干扰下一次操作
|
||||
await expect(self.page.locator('.el-message__content').filter(has_text='删除成功')).to_be_hidden(
|
||||
timeout=5000
|
||||
)
|
||||
|
||||
if verify:
|
||||
# 验证删除成功
|
||||
# 重新搜索验证不存在
|
||||
await self.search_table(table_name)
|
||||
# 使用 strict matching
|
||||
row = self.page.locator('.app-container .el-table__body-wrapper tbody tr').filter(
|
||||
has=self.page.get_by_text(table_name, exact=True)
|
||||
)
|
||||
# 期望找不到或者 count为0
|
||||
await expect(row).to_have_count(0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gen_table_page() -> None:
|
||||
"""测试代码生成页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = GenTableTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.test_gen_code_flow()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
@@ -0,0 +1,38 @@
|
||||
import pytest
|
||||
from playwright.async_api import async_playwright, expect
|
||||
|
||||
from common.base_page_test import BasePageTest
|
||||
from common.config import Config
|
||||
|
||||
|
||||
class SwaggerTest(BasePageTest):
|
||||
async def check_swagger_interface(self) -> None:
|
||||
"""测试系统接口页面 (Swagger UI)"""
|
||||
|
||||
# 1. 直接导航到系统接口页面
|
||||
await self.goto_page(Config.frontend_url + '/tool/swagger')
|
||||
|
||||
# 2. 验证页面加载
|
||||
# 等待 iframe 出现
|
||||
iframe = self.page.locator('iframe')
|
||||
await expect(iframe).to_be_visible()
|
||||
|
||||
# 获取 iframe 内容框架
|
||||
frame = self.page.frame_locator('iframe')
|
||||
|
||||
# 后端未禁用Swagger时,验证 iframe 内部的标题包含 "ShiErFeng-FastAPI"
|
||||
# 当前生产环境已默认禁用Swagger,此处验证标题是否包含默认禁用提示
|
||||
h1_locator = frame.locator('h1')
|
||||
await expect(h1_locator).to_contain_text('Swagger UI has been disabled. Please enable it first.', timeout=15000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_swagger_page() -> None:
|
||||
"""测试系统接口页面功能"""
|
||||
async with async_playwright() as p:
|
||||
test_instance = SwaggerTest()
|
||||
await test_instance.setup(p)
|
||||
try:
|
||||
await test_instance.check_swagger_interface()
|
||||
finally:
|
||||
await test_instance.teardown()
|
||||
Reference in New Issue
Block a user