基于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 中的选择器配置是否正确

📝 开发建议

  1. 添加依赖管理:创建 requirements.txt 文件
  2. 改进错误处理:增加更详细的异常处理和日志
  3. 扩展测试覆盖:添加更多业务场景的测试用例
  4. CI/CD集成:配置持续集成流程
  5. 报告优化:集成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
posted @ 2025-12-19 15:42  乐乐乐乐乐乐樂  阅读(0)  评论(0)    收藏  举报
jQuery火箭图标返回顶部代码