from __future__ import annotations from collections.abc import Callable from pathlib import Path from typing import Pattern from playwright.sync_api import Download, Page, expect def login( page: Page, base_url: str, *, username: str, password: str, expected_path: str | Pattern[str] | None = None, ) -> None: page.goto(f"{base_url}/auth/login") expect(page.get_by_label("用户名")).to_be_editable() page.get_by_label("用户名").fill(username) page.get_by_label("密码").fill(password) page.get_by_role("button", name="登录").click() if expected_path is not None: expect_path(page, base_url, expected_path) def logout(page: Page, base_url: str) -> None: open_user_menu(page) page.get_by_role("menuitem", name="退出登录").click() expect_path(page, base_url, "/auth/login") expect_flash(page, "您已退出登录。") def open_user_menu(page: Page) -> None: toggle = page.locator("#user-menu-toggle") expect(toggle).to_be_visible() if toggle.get_attribute("aria-expanded") != "true": toggle.click() expect(page.locator("#user-menu-panel")).to_be_visible() def accept_next_dialog(page: Page, expected_message_substring: str | None = None) -> None: def _accept(dialog) -> None: if expected_message_substring is not None: assert expected_message_substring in dialog.message dialog.accept() page.once("dialog", _accept) def dismiss_next_dialog(page: Page, expected_message_substring: str | None = None) -> None: def _dismiss(dialog) -> None: if expected_message_substring is not None: assert expected_message_substring in dialog.message dialog.dismiss() page.once("dialog", _dismiss) def expect_sidebar_link(page: Page, link_name: str) -> None: toggle_count = page.locator("[data-sidebar-toggle]").count() for index in range(toggle_count): toggle = page.locator("[data-sidebar-toggle]").nth(index) if not toggle.is_visible(): continue if toggle.get_attribute("aria-expanded") == "false": toggle.click() break expect(page.get_by_role("link", name=link_name)).to_be_visible() def expect_flash(page: Page, message: str, *, category: str | None = None) -> None: locator = page.get_by_text(message) expect(locator).to_be_visible() if category is not None: expect(page.get_by_text(f"{category}:{message}")).to_be_visible() def expect_path(page: Page, base_url: str, expected_path: str | Pattern[str]) -> None: if isinstance(expected_path, str): expect(page).to_have_url(f"{base_url}{expected_path}") return expect(page).to_have_url(expected_path) def upload_file(page: Page, selector: str, file_path: str | Path) -> None: input_locator = page.locator(selector) expect(input_locator).to_be_attached() input_locator.set_input_files(str(file_path)) def download_via_action(page: Page, action: Callable[[], None], destination: str | Path) -> Download: destination_path = Path(destination) destination_path.parent.mkdir(parents=True, exist_ok=True) with page.expect_download() as download_info: action() download = download_info.value download.save_as(str(destination_path)) return download