Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-test/tool/test_code_gen.py
T
insistence 025b0822df refactor: 统一多前端工程架构、插件配置与 CI (#131)
* refactor: 重构项目架构

* refactor: 重构ci和测试

* fix: 修复测试

* feat: 新增更多E2E测试

* fix: 修复E2E测试
2026-10-10 21:15:22 +08:00

165 lines
8.1 KiB
Python

import asyncio
import re
from http import HTTPStatus
from pathlib import Path
from urllib.parse import urlparse
from uuid import uuid4
from zipfile import ZipFile
import pytest
from playwright.async_api import Locator, expect
from common.base_page_test import BasePageTest
from common.browser_harness import BrowserHarness
from common.config import Config
pytestmark = pytest.mark.e2e
class GenTableTest(BasePageTest):
"""只操作本用例导入的生成配置,验证编辑、代码预览及下载。"""
table_id: int | None = None
table_name: str = ''
table_comment: str = ''
def table_row(self) -> Locator:
return self.page.locator('.app-container:visible > .el-table .el-table__body-wrapper tbody tr').filter(
has=self.page.get_by_text(self.table_name, exact=True)
)
async def search_table(self) -> dict:
"""等待搜索接口与表格完成,避免初始响应覆盖查询结果。"""
form = self.page.locator('form').first
await form.locator('.el-form-item').filter(has_text='表名称').locator('input').fill(self.table_name)
async with self.page.expect_response(
lambda response: urlparse(response.url).path.endswith('/tool/gen/list')
) as pending:
await form.get_by_role('button', name=re.compile(r'搜索$')).click()
payload = await (await pending.value).json()
assert payload['code'] == HTTPStatus.OK, payload
await expect(self.page.locator('.el-table .el-loading-mask:visible')).to_have_count(0)
return payload
async def import_table(self) -> None:
"""从未导入的真实表中选择一张,不删除任何既有生成配置。"""
async with self.page.expect_response(
lambda response: urlparse(response.url).path.endswith('/tool/gen/db/list')
) as pending:
await self.page.get_by_role('button', name=re.compile(r'导入$')).click()
payload = await (await pending.value).json()
assert payload['code'] == HTTPStatus.OK and payload['rows'], payload
self.table_name = payload['rows'][0]['tableName']
dialog = self.page.get_by_role('dialog', name='导入表', exact=True)
row = dialog.locator('tbody tr').filter(has=self.page.get_by_text(self.table_name, exact=True))
await row.locator('.el-checkbox').click()
await expect(row.locator('.el-checkbox')).to_have_class(re.compile('is-checked'))
async with self.page.expect_response(
lambda response: (
response.request.method == 'POST' and urlparse(response.url).path.endswith('/tool/gen/importTable')
)
) as pending:
await dialog.get_by_role('button', name=re.compile(r'确\s*定$')).click()
result = await (await pending.value).json()
assert result['code'] == HTTPStatus.OK, result
await expect(dialog).to_be_hidden()
listed = await self.search_table()
assert len(listed['rows']) == 1, listed
self.table_id = listed['rows'][0]['tableId']
await expect(self.table_row()).to_have_count(1)
async def edit_table(self) -> None:
"""修改实际生成配置,并重新读取确认服务端持久化结果。"""
await self.table_row().locator('td').last.get_by_role('button').nth(1).click()
await self.page.wait_for_url(f'**/tool/gen-edit/index/{self.table_id}**')
self.table_comment = f'生成验收{uuid4().hex[:8]}'
await self.page.get_by_role('tab', name='基本信息', exact=True).click()
await (
self.page.locator('.el-form-item:visible')
.filter(has_text='表描述')
.locator('input')
.fill(self.table_comment)
)
async with self.page.expect_response(
lambda response: response.request.method == 'PUT' and urlparse(response.url).path.endswith('/tool/gen')
) as pending:
await self.page.get_by_role('button', name=re.compile(r'提交$')).click()
result = await (await pending.value).json()
assert result['code'] == HTTPStatus.OK, result
await self.page.wait_for_url(re.compile(r'/tool/gen(?:\?.*)?$'))
listed = await self.search_table()
assert listed['rows'][0]['tableComment'] == self.table_comment
await expect(self.table_row()).to_contain_text(self.table_comment)
async def preview_and_download(self) -> None:
"""核对预览接口、可见 Python 代码和实际下载 ZIP 的内容。"""
async with self.page.expect_response(
lambda response: urlparse(response.url).path.endswith(f'/tool/gen/preview/{self.table_id}')
) as pending:
await self.table_row().locator('td').last.get_by_role('button').nth(0).click()
payload = await (await pending.value).json()
assert payload['code'] == HTTPStatus.OK, payload
code = payload['data']
assert any(name.endswith('do.py.jinja2') for name in code), code.keys()
assert any(self.table_comment in source for source in code.values())
dialog = self.page.get_by_role('dialog', name='代码预览', exact=True)
await expect(dialog.locator('pre:visible').first).to_contain_text('class')
await expect(dialog.locator('pre:visible').first).to_contain_text(self.table_comment)
await dialog.locator('.el-dialog__headerbtn').click()
async with self.page.expect_download() as pending:
await self.table_row().locator('td').last.get_by_role('button').nth(4).click()
download = await pending.value
assert download.suggested_filename.endswith('.zip')
zip_path = Path(await download.path())
def read_generated_code() -> tuple[list[str], str]:
with ZipFile(zip_path) as archive:
names = archive.namelist()
return names, '\n'.join(archive.read(name).decode('utf-8') for name in names if name.endswith('.py'))
names, generated = await asyncio.to_thread(read_generated_code)
assert any(name.endswith('.vue') for name in names), names
assert any(name.endswith('.py') for name in names), names
assert self.table_comment in generated and self.table_name in generated
async def delete_table(self) -> None:
"""通过界面移除刚创建的元数据,并确认实际业务表仍存在。"""
await self.table_row().locator('td').last.get_by_role('button').nth(2).click()
async with self.page.expect_response(
lambda response: (
response.request.method == 'DELETE'
and urlparse(response.url).path.endswith(f'/tool/gen/{self.table_id}')
)
) as pending:
await self.page.get_by_role('button', name=re.compile(r'确定$')).click()
assert (await (await pending.value).json())['code'] == HTTPStatus.OK
self.table_id = None
await self.search_table()
await expect(self.table_row()).to_have_count(0)
api = await self.harness.api_context(self.token)
available = await api.get('/tool/gen/db/list', params={'tableName': self.table_name})
assert any(row['tableName'] == self.table_name for row in (await available.json())['rows'])
async def cleanup(self) -> None:
"""失败时仍只清理由本次导入取得 ID 的元数据。"""
if self.table_id is not None:
api = await self.harness.api_context(self.token)
response = await api.delete(f'/tool/gen/{self.table_id}')
assert (await response.json())['code'] == HTTPStatus.OK
@pytest.mark.asyncio
async def test_gen_table_page(browser_harness: BrowserHarness) -> None:
"""测试真实数据库表的导入、编辑、预览、下载与删除。"""
test = GenTableTest()
await test.setup(browser_harness)
try:
async with test.page.expect_response(lambda response: urlparse(response.url).path.endswith('/tool/gen/list')):
await test.page.goto(Config.frontend_url + '/tool/gen')
await test.import_table()
await test.edit_table()
await test.preview_and_download()
await test.delete_table()
finally:
await test.cleanup()