基于Playwright的Web自动化测试框架,采用Page Object Model设计模式,支持登录认证和搜索功能的自动化测试。
Web自动化测试项目
基于Playwright的Web自动化测试框架,采用Page Object Model设计模式,支持登录认证和搜索功能的自动化测试。
🚀 项目特性
- Playwright框架:现代化的Web自动化测试工具
- Page Object Model:清晰的页面对象模型设计
- 认证管理:支持登录状态保存和复用
- 大视口支持:1920x1080分辨率测试
- 截图功能:自动截图记录测试过程
- 灵活配置:集中化的测试数据和选择器管理
📁 项目结构
├── auth/ # 认证文件存储
│ └── auth.json # 登录状态保存文件
├── config/ # 配置文件
│ ├── test_data.py # 测试数据配置
│ └── __init__.py
├── pages/ # 页面对象模型
│ ├── base_bage.py # 基础页面类
│ ├── login_page.py # 登录页面
│ ├── search_page.py # 搜索页面
│ └── __init__.py
├── screenshots/ # 截图存储
├── testcases/ # 测试用例
│ ├── test_login.py # 登录测试
│ ├── test_search.py # 搜索测试
│ └── __init__.py
├── utils/ # 工具类
│ ├── auth_manage.py # 认证管理工具
│ └── __init__.py
├── conftest.py # pytest配置
├── pytest.ini # pytest设置
└── main.py # 主程序入口
🛠️ 环境要求
- Python 3.8+
- Playwright
- pytest
📦 安装依赖
# 安装Python依赖
pip install playwright pytest
# 安装浏览器
playwright install chromium
⚙️ 配置说明
测试数据配置 (config/test_data.py)
# 登录凭据
CREDENTIALS = {
'username': 'your_email@example.com',
'password': 'your_password'
}
# URL配置
URLS = {
"base_url": "https://your-domain.com/#",
"login_page": "/login",
"search_page": "/search",
}
pytest配置 (pytest.ini)
- 测试路径:
testcases/ - 浏览器模式:有头模式(
--headed) - 执行速度:慢速模式(
--slowmo=500) - 失败策略:遇到第一个失败即停止
🚀 使用方法
1. 获取登录认证
首次使用需要获取登录认证状态:
python utils/auth_manage.py
这将:
- 启动浏览器并执行登录
- 保存认证状态到
auth/auth.json - 后续测试可复用此认证状态
2. 运行测试
# 运行所有测试
pytest
# 运行特定测试文件
pytest testcases/test_login.py
pytest testcases/test_search.py
# 运行特定测试方法
pytest testcases/test_login.py::TestLogin::test_succesful_login
# 详细输出
pytest -v
# 生成HTML报告
pytest --html=report.html
3. 查看测试结果
- 截图:测试过程截图保存在
screenshots/目录 - 日志:控制台输出详细的执行日志
- 认证文件:登录状态保存在
auth/auth.json
🧪 测试用例说明
登录测试 (test_login.py)
test_succesful_login:测试成功登录流程- 导航到登录页面
- 处理视频弹窗
- 输入用户名密码
- 执行登录操作
- 验证登录结果
搜索测试 (test_search.py)
test_search_with_keyword:测试关键词搜索功能- 使用已认证状态
- 导航到搜索页面
- 输入主关键词和辅助关键词
- 执行搜索操作
- 截图验证结果
🔧 核心功能
认证管理
- 状态保存:登录后自动保存认证状态
- 状态复用:测试用例可直接使用已保存的认证
- 自动处理:conftest.py中自动处理认证相关的fixture
页面对象模型
- BasePage:提供通用的页面操作方法
- LoginPage:封装登录页面的所有操作
- SearchPage:封装搜索页面的所有操作
视口管理
- 大视口模式:强制使用1920x1080分辨率
- 自动调整:页面导航时自动保持大视口
- 多重保障:多种方式确保视口设置生效
🐛 常见问题
1. 认证文件不存在
认证文件不存在,请先运行: python utils/auth_manage.py
解决方案:运行认证管理工具获取登录状态
2. 视频弹窗干扰
项目已内置视频弹窗处理机制:
- JavaScript隐藏弹窗
- ESC键关闭
- 点击其他区域
3. 元素定位失败
检查 config/test_data.py 中的选择器配置是否正确
📝 开发建议
- 添加依赖管理:创建
requirements.txt文件 - 改进错误处理:增加更详细的异常处理和日志
- 扩展测试覆盖:添加更多业务场景的测试用例
- CI/CD集成:配置持续集成流程
- 报告优化:集成Allure等测试报告工具
📄 许可证
本项目采用MIT许可证,详见LICENSE文件。
🤝 贡献
欢迎提交Issue和Pull Request来改进项目。
注意:请确保在运行测试前已正确配置测试数据和获取了有效的认证状态。
config/test_data.py
# -*- coding: utf-8 -*-
"""
测试数据配置
"""
class TestData:
"""
测试数据配置
"""
# 登录凭据
CREDENTIALS = {
'username': 'luckyletop@163.com',
'password': '123456'
}
# url配置
URLS = {
"base_url": "https://xxxx.xxxx.com/#", # 基础地址
"login_page":"/login",
"search_page":"/search",
}
# 超时配置(毫秒)
TIMEOUTS = {
"short": 5000,
"medium": 10000,
"long": 30000
}
# 选择器配置
SELECTORS = {
"email_input":'input[type="email"]',
"password_input":'input[type="password"]',
"login_button":'button[type="submit"]',
"add_subject_folder":"#icon-add-project_椭圆形",
"main_input":'input[placeholder="主关键词"]',
"assist_input":'input[placeholder="辅助关键词"]',
"search_button" :'//button[@type="button" and @style="height: 76px;"]'
}
SEARCH_KEYWORDS = {
"main_input":'中国政府',
"assist_input":'发布'
}
page/base_page.py
# -*- coding: utf-8 -*-
from playwright.sync_api import Page
from typing import Optional
import time
class BasePage:
def __init__(self, page: Page):
self.page = page
self.timeout = 10000
def navigate_to(self, url: str):
"""导航到指定url"""
self.page.goto(url)
self.page.wait_for_load_state('networkidle')
def navigate_to_document(self, url: str):
"""导航到指定url"""
self.page.goto(url)
self.page.wait_for_load_state('domcontentloaded')
def click_element(self,selector: str, timeout: Optional[int] = None):
"""点击元素"""
timeout = timeout or self.timeout
self.page.wait_for_selector(selector, timeout=timeout)
self.page.click(selector)
def fill_input(self, selector: str, text: str, timeout: Optional[int] = None):
"""输入文本"""
timeout = timeout or self.timeout
self.page.wait_for_selector(selector, timeout=timeout)
self.page.fill(selector, text)
def get_text(self, selector: str, timeout: Optional[int] = None):
"""获取文本"""
timeout = timeout or self.timeout
self.page.wait_for_selector(selector, timeout=timeout)
return self.page.text_content(selector)
def wait_for_element(self, selector: str, timeout: Optional[int] = None):
"""等待元素加载"""
timeout = timeout or self.timeout
self.page.wait_for_selector(selector, timeout=timeout)
def is_element_visible(self, selector: str, timeout: Optional[int] = None):
"""判断元素是否可见"""
try:
return self.page.is_visible(selector)
except:
return False
def take_screenshot(self, name: str):
"""截图"""
self.page.screenshot(path=f"screenshots/{name}.png")
def wait_for_page_load(self):
"""等待页面加载完成"""
self.page.wait_for_load_state('networkidle')
def scroll_to_element(self, selector: str):
"""滚动到元素"""
self.page.locator(selector).scroll_into_view_if_needed()
def get_current_url(self):
"""获取当前页面url"""
return self.page.url
def refresh_page(self):
"""刷新页面"""
self.page.reload()
self.wait_for_page_load()
def wait_for_timeout(self, timeout: int):
"""等待指定时间"""
self.page.wait_for_timeout(timeout)
pages/login_page.py
# -*- coding: utf-8 -*-
"""
登录页面对象模型
"""
from playwright.sync_api import Page
from pages.base_bage import BasePage
from config.test_data import TestData
class Loginpage(BasePage):
def __init__(self,page:Page):
super().__init__(page)
self.email_input = TestData.SELECTORS["email_input"]
self.password_input = TestData.SELECTORS["password_input"]
self.login_button = TestData.SELECTORS["login_button"]
def navigate_to_login(self) -> None:
"""导航到登录页面"""
self.navigate_to(TestData.URLS["base_url"])
# 关闭可能的视频弹窗
self.close_video_modal()
def fill_email(self,email:str) -> None:
"""输入邮箱"""
self.fill_input(self.email_input,email)
def fill_password(self,password:str) -> None:
"""输入密码"""
self.fill_input(self.password_input,password)
def click_login_button(self) -> None:
"""点击登录按钮"""
print(f"准备点击登录按钮,选择器: {self.login_button}")
# 再次确保视频弹窗已关闭
self.close_video_modal()
# 等待按钮可见并可点击
self.page.wait_for_selector(self.login_button, state="visible")
# 滚动到按钮位置
self.scroll_to_element(self.login_button)
# 直接使用JavaScript点击,这是最可靠的方法
self.page.evaluate('document.querySelector("button[type=\\"submit\\"]").click()')
print("登录按钮已点击(JavaScript点击)")
def login(self,email:str,password:str) -> None:
"""登录"""
print(f"开始登录流程,用户名: {email}")
# self.navigate_to_login()
self.fill_email(email)
print("邮箱已填入")
self.fill_password(password)
print("密码已填入")
# 等待一下确保输入完成
self.wait_for_timeout(1000)
self.click_login_button()
self.wait_for_page_load()
print("登录流程完成")
def close_video_modal(self) -> None:
"""关闭视频弹窗"""
try:
print("开始处理视频弹窗...")
# 方法1: 直接隐藏视频弹窗
video_modal_script = """
() => {
// 隐藏所有视频相关的弹窗
const videoModals = document.querySelectorAll('.vjs-modal-dialog-content, .vjs-error-display, .video-box, .vjs-modal-dialog');
videoModals.forEach(modal => {
if (modal) {
modal.style.display = 'none';
modal.style.visibility = 'hidden';
modal.style.zIndex = '-1';
}
});
// 隐藏整个视频容器
const videoContainers = document.querySelectorAll('.video-js, [class*="video"]');
videoContainers.forEach(container => {
if (container && container.classList.contains('vjs-error')) {
container.style.display = 'none';
}
});
return 'Video modals hidden';
}
"""
result = self.page.evaluate(video_modal_script)
print(f"视频弹窗处理结果: {result}")
# 方法2: 按ESC键
self.page.keyboard.press('Escape')
self.wait_for_timeout(500)
# 方法3: 点击页面其他区域来关闭弹窗
try:
self.page.click('body', position={'x': 100, 'y': 100})
self.wait_for_timeout(500)
except:
pass
print("视频弹窗处理完成")
except Exception as e:
print(f"关闭视频弹窗时出错: {e}")
# 尝试按ESC键作为备选方案
try:
self.page.keyboard.press('Escape')
self.wait_for_timeout(1000)
except:
pass
def login_with_credentials(self) -> None:
"""使用默认的登录凭据登录"""
credentials = TestData.CREDENTIALS
self.login(credentials["username"],credentials["password"])
pages/search_page
# -*- coding: utf-8 -*-
"""
搜索
"""
import pytest
from playwright.sync_api import Page
from pages.base_bage import BasePage
from config.test_data import TestData
from pathlib import Path
class SearchPage(BasePage):
def __init__(self,page:Page):
super().__init__(page)
self.main_input= TestData.SELECTORS["main_input"]
self.assist_input= TestData.SELECTORS["assist_input"]
self.search_button= TestData.SELECTORS["search_button"]
def navigate_to_search(self) -> None:
"""导航到搜索页面"""
try:
print('访问基础域名')
base_url = TestData.URLS["base_url"].replace("/#","")
self.navigate_to_document(base_url)
# 等待页面稳定
self.wait_for_timeout(2000)
# 导航到搜索页面
search_url = TestData.URLS["base_url"]+TestData.URLS["search_page"]
self.navigate_to_document(search_url)
self.wait_for_timeout(2000)
except Exception as e:
print(f"导航到搜索页面时出错: {e}")
def get_page_info(self):
"""获取页面信息"""
current_url = self.get_current_url()
title = self.page.title()
info = {
"url": current_url,
"title": title
}
print(f"📊 页面信息:")
print(f" URL: {info['url']}")
print(f" 标题: {info['title']}")
return info
def fail_main_input(self,main_input:str):
"""输入主关键词"""
self.fill_input(self.main_input, main_input)
def fail_assist_input(self,assist_input:str):
"""输入辅助关键词"""
self.fill_input(self.assist_input,assist_input)
def click_search_button(self):
"""点击搜索按钮"""
self.click_element(TestData.SELECTORS["search_button"])
def search_kw(self,main_input:str,assist_input:str):
"""搜索"""
self.fail_main_input(main_input)
self.fail_assist_input(assist_input)
# 等待下
self.wait_for_timeout(1000)
self.click_search_button()
self.wait_for_page_load()
print('搜索完成')
self.wait_for_timeout(3000)
def search_with_kw(self):
"""使用默认的搜索参数"""
searchkw = TestData.SEARCH_KEYWORDS
self.search_kw(searchkw["main_input"],searchkw["assist_input"])
testcases/test_login.py
# -*- coding: utf-8 -*-
"""
登录功能测试
"""
import os
import sys
import pytest
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from playwright.sync_api import Page
from pages.login_page import Loginpage
from config.test_data import TestData
class TestLogin:
"""登录测试类"""
def test_succesful_login(self, page: Page):
"""测试成功登录(使用无认证页面)"""
print("开始测试登录功能")
login_page = Loginpage(page)
# 导航到登录页面
print("导航到登录页面")
login_page.navigate_to_login()
# 等待页面加载完成
print("等待页面加载")
page.wait_for_timeout(3000)
#
# # 截图查看页面状态
# login_page.take_screenshot("before_login")
# 执行登录
print("开始执行登录")
login_page.login_with_credentials()
# 等待登录完成
print("等待登录完成")
page.wait_for_timeout(5000)
# 截图验证
login_page.take_screenshot("after_login")
print("测试完成")
# def test_already_logged_in(self, auth_page: Page):
# """测试已登录状态访问登录页面(使用带认证页面)"""
# print("开始测试已登录状态访问登录页面")
#
# # 直接访问登录页面
# auth_page.goto(TestData.URLS["base_url"])
# auth_page.wait_for_load_state('networkidle')
# auth_page.wait_for_timeout(3000)
#
# current_url = auth_page.url
# print(f"当前页面URL: {current_url}")
#
# # 截图验证
# auth_page.screenshot(path="screenshots/already_logged_in.png")
#
# # 验证是否被重定向到主页面(而不是停留在登录页)
# if "login" not in current_url.lower():
# print("✅ 已登录用户被正确重定向到主页面")
# else:
# print("⚠️ 已登录用户仍在登录页面,需要检查认证状态")
#
# print("测试完成")
testcases/test_search.py
# -*- coding: utf-8 -*-
"""
搜索
"""
import os
import sys
import pytest
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from playwright.sync_api import Page
from config.test_data import TestData
from pages.search_page import SearchPage
class TestSearch:
# def test_search(self,auth_page:Page):
# # 创建SearchPage对象
# search_page = SearchPage(auth_page)
# search_page.navigate_to_search()
# page_info = search_page.get_page_info()
# assert page_info["url"], "页面URL不能为空"
# assert page_info["title"], "页面标题不能为空"
def test_search_with_keyword(self, auth_page: Page):
# 创建SearchPage对象
search_page = SearchPage(auth_page)
try:
search_page.navigate_to_search()
# 等待页面加载完成
search_page.wait_for_timeout(1000)
# 执行关键词搜索
search_page.search_with_kw()
search_page.wait_for_timeout(5000)
# 截图验证
search_page.take_screenshot("search_result")
print("测试完成")
except Exception as e:
# 异常处理
print(f"测试执行过程中发生错误: {str(e)}")
# 可以选择重新截图记录错误状态
search_page.take_screenshot("search_error")
# 重新抛出异常让pytest处理
raise e
utils/auth_manage.py
# -*- coding: utf-8 -*-
import sys
import os
from pathlib import Path
from playwright.sync_api import Browser
# 添加项目根目录到路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from pages.login_page import Loginpage
def standalone_login():
"""独立执行登录并保存认证状态"""
from playwright.sync_api import sync_playwright
from config.test_data import TestData
print("开始独立执行登录流程...")
with sync_playwright() as p:
# 启动浏览器
browser = p.chromium.launch(
headless=False, # 有头模式,可以看到登录过程
slow_mo=500,
args=[
'--start-maximized',
'--disable-blink-features=AutomationControlled',
'--disable-web-security',
'--window-size=1920,1080',
]
)
try:
# 创建上下文和页面
context = browser.new_context(
viewport=None,
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
)
page = context.new_page()
page.set_viewport_size({"width": 1920, "height": 1080})
# 执行登录
login_page = Loginpage(page)
login_page.navigate_to_login()
page.wait_for_timeout(3000)
print("开始执行登录...")
login_page.login_with_credentials()
page.wait_for_timeout(5000)
# 保存认证状态
auth_dir = Path("auth")
auth_dir.mkdir(parents=True, exist_ok=True)
storage_path = auth_dir / "auth.json"
context.storage_state(path=storage_path)
print(f"✅ 登录成功!认证状态已保存到: {storage_path}")
print(f"📁 认证文件路径: {storage_path.absolute()}")
# 验证认证文件
if storage_path.exists():
print(f"📊 认证文件大小: {storage_path.stat().st_size} 字节")
print("🎉 认证状态获取完成!")
else:
print("❌ 认证文件保存失败")
except Exception as e:
print(f"❌ 登录过程中出错: {e}")
raise
finally:
page.close()
context.close()
browser.close()
if __name__ == "__main__":
print("🚀 开始获取登录认证状态...")
standalone_login()
conftest.py
# -*- coding: utf-8 -*-
"""
Playwright 测试配置 - 优化版本
"""
import pytest
import os
import sys
from pathlib import Path
from playwright.sync_api import Browser, BrowserContext, Page
# 添加项目根目录到路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from pages.login_page import Loginpage
from config.test_data import TestData
# 常量配置
VIEWPORT_SIZE = {"width": 1920, "height": 1080}
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
AUTH_FILE_PATH = Path("auth/auth.json")
def pytest_configure(config):
"""配置pytest"""
Path("screenshots").mkdir(exist_ok=True)
def setup_large_viewport(page: Page):
"""设置并保持大视口的通用函数"""
def ensure_large_viewport():
try:
page.set_viewport_size(VIEWPORT_SIZE)
print(f"🔧 已强制设置视口: {VIEWPORT_SIZE['width']}x{VIEWPORT_SIZE['height']}")
except Exception as e:
print(f"⚠️ 设置视口失败: {e}")
# 在页面导航时保持大视口
page.on("framenavigated", lambda frame: ensure_large_viewport() if frame == page.main_frame else None)
# 页面加载完成后也强制设置
page.on("load", lambda: ensure_large_viewport())
# 初始设置(多次确保)
ensure_large_viewport()
# 延迟设置,确保页面完全加载后再设置
def delayed_setup():
page.wait_for_timeout(500)
ensure_large_viewport()
# 在后台执行延迟设置
try:
page.evaluate("setTimeout(() => {}, 100)") # 触发异步执行
delayed_setup()
except:
pass
def get_context_args(storage_state=None):
"""获取浏览器上下文参数的通用函数"""
args = {
"viewport": VIEWPORT_SIZE,
"user_agent": USER_AGENT
}
if storage_state:
args["storage_state"] = storage_state
return args
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args):
"""配置浏览器启动参数"""
slow_mo = int(os.getenv("SLOW_MO", "500"))
print("📱 启用大视口模式 (1920x1080)")
return {
**browser_type_launch_args,
"headless": False,
"slow_mo": slow_mo,
"args": [
"--start-maximized",
"--disable-blink-features=AutomationControlled",
"--disable-infobars",
"--window-size=1920,1080",
"--window-position=0,0",
"--force-device-scale-factor=1",
]
}
@pytest.fixture(scope="function")
def browser_context_args(browser_context_args):
"""配置浏览器上下文参数"""
return {**browser_context_args, **get_context_args()}
@pytest.fixture(scope="function")
def page(context: BrowserContext):
"""页面实例 - 保持大视口"""
page = context.new_page()
setup_large_viewport(page)
yield page
page.close()
@pytest.fixture(scope="function")
def auth_context(browser: Browser):
"""浏览器上下文 - 带认证"""
if not AUTH_FILE_PATH.exists():
pytest.skip("认证文件不存在,请先运行: python utils/auth_manage.py")
context = browser.new_context(**get_context_args(str(AUTH_FILE_PATH)))
yield context
context.close()
@pytest.fixture(scope="function")
def auth_page(auth_context: BrowserContext):
"""页面实例 - 带认证,保持大视口"""
page = auth_context.new_page()
setup_large_viewport(page)
yield page
page.close()
def create_authenticated_page(browser: Browser):
"""创建带认证的页面 - 工具函数"""
if not AUTH_FILE_PATH.exists():
raise FileNotFoundError("认证文件不存在,请先运行: python utils/auth_manage.py")
context = browser.new_context(**get_context_args(str(AUTH_FILE_PATH)))
page = context.new_page()
setup_large_viewport(page)
return page, context
pytest.ini
[tool:pytest]
testpaths = testcases
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--verbose
--tb=short
--maxfail = 1
--headed
--slowmo=500

浙公网安备 33010602011771号