From 03e152baabaaa3a282bf09ed1af96dc49f5cfbf9 Mon Sep 17 00:00:00 2001 From: xst Date: Wed, 15 Jul 2026 14:02:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=20=E2=80=94=20=E8=A6=86=E7=9B=96=206=20?= =?UTF-8?q?=E7=AB=AF=20(=E7=AE=A1=E7=90=86=E7=AB=AF=20+=20=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=20+=204=20=E4=B8=AA=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目录结构: tests/web/admin/ 管理端 (Playwright) tests/web/mini_program/ 司机端小程序 (Playwright/微信 WebView) tests/app/driver_android/ 司机安卓 (Appium) tests/app/shipper_android/ 货主安卓 (Appium) tests/app/driver_ios/ 司机苹果 (Appium) tests/app/shipper_ios/ 货主苹果 (Appium) 公共模块: tests/app/_shared.py driver+ensure_logged_in 工厂 tests/app/_base_page.py App Page Object 基类 配置: test_config.yaml 拆分为 6 个独立 section + 货主账号 --- .../app/{pages/base_page.py => _base_page.py} | 36 +--- tests/app/_shared.py | 125 +++++++++++ tests/app/conftest.py | 198 ------------------ tests/app/driver_android/conftest.py | 27 +++ .../driver_android/pages/driver_home_page.py | 26 +++ tests/app/driver_android/tests/test_smoke.py | 18 ++ tests/app/driver_ios/conftest.py | 27 +++ tests/app/driver_ios/pages/__init__.py | 0 tests/app/driver_ios/tests/__init__.py | 0 tests/app/driver_ios/tests/test_smoke.py | 18 ++ tests/app/pages/driver_home_page.py | 47 ----- tests/app/shipper_android/conftest.py | 27 +++ tests/app/shipper_android/pages/__init__.py | 0 tests/app/shipper_android/tests/__init__.py | 0 tests/app/shipper_android/tests/test_smoke.py | 18 ++ tests/app/shipper_ios/conftest.py | 27 +++ tests/app/shipper_ios/pages/__init__.py | 0 tests/app/shipper_ios/tests/__init__.py | 0 tests/app/shipper_ios/tests/test_smoke.py | 18 ++ tests/app/test_login/test_login.py | 108 ---------- tests/config/test_config.py | 6 +- tests/config/test_config.yaml | 137 ++++++++---- tests/conftest.py | 48 ++++- tests/pytest.ini | 10 +- tests/web/admin/conftest.py | 126 +++++++++++ tests/web/{ => admin}/pages/base_page.py | 0 .../{ => admin}/pages/components/navbar.py | 4 +- tests/web/{ => admin}/pages/login_page.py | 4 +- tests/web/{ => admin}/save_auth.py | 6 +- tests/web/admin/test_login/test_login.py | 74 +++++++ tests/web/auth/super_admin_chromium.json | 1 + tests/web/conftest.py | 180 ---------------- tests/web/mini_program/conftest.py | 52 +++++ tests/web/mini_program/pages/__init__.py | 31 +++ .../web/mini_program/pages/mini_home_page.py | 24 +++ tests/web/mini_program/tests/test_smoke.py | 16 ++ tests/web/test_login/test_login.py | 111 ---------- 37 files changed, 823 insertions(+), 727 deletions(-) rename tests/app/{pages/base_page.py => _base_page.py} (66%) create mode 100644 tests/app/_shared.py delete mode 100644 tests/app/conftest.py create mode 100644 tests/app/driver_android/conftest.py create mode 100644 tests/app/driver_android/pages/driver_home_page.py create mode 100644 tests/app/driver_android/tests/test_smoke.py create mode 100644 tests/app/driver_ios/conftest.py create mode 100644 tests/app/driver_ios/pages/__init__.py create mode 100644 tests/app/driver_ios/tests/__init__.py create mode 100644 tests/app/driver_ios/tests/test_smoke.py delete mode 100644 tests/app/pages/driver_home_page.py create mode 100644 tests/app/shipper_android/conftest.py create mode 100644 tests/app/shipper_android/pages/__init__.py create mode 100644 tests/app/shipper_android/tests/__init__.py create mode 100644 tests/app/shipper_android/tests/test_smoke.py create mode 100644 tests/app/shipper_ios/conftest.py create mode 100644 tests/app/shipper_ios/pages/__init__.py create mode 100644 tests/app/shipper_ios/tests/__init__.py create mode 100644 tests/app/shipper_ios/tests/test_smoke.py delete mode 100644 tests/app/test_login/test_login.py create mode 100644 tests/web/admin/conftest.py rename tests/web/{ => admin}/pages/base_page.py (100%) rename tests/web/{ => admin}/pages/components/navbar.py (92%) rename tests/web/{ => admin}/pages/login_page.py (94%) rename tests/web/{ => admin}/save_auth.py (95%) create mode 100644 tests/web/admin/test_login/test_login.py create mode 100644 tests/web/auth/super_admin_chromium.json delete mode 100644 tests/web/conftest.py create mode 100644 tests/web/mini_program/conftest.py create mode 100644 tests/web/mini_program/pages/__init__.py create mode 100644 tests/web/mini_program/pages/mini_home_page.py create mode 100644 tests/web/mini_program/tests/test_smoke.py delete mode 100644 tests/web/test_login/test_login.py diff --git a/tests/app/pages/base_page.py b/tests/app/_base_page.py similarity index 66% rename from tests/app/pages/base_page.py rename to tests/app/_base_page.py index 92b2e27..1100023 100644 --- a/tests/app/pages/base_page.py +++ b/tests/app/_base_page.py @@ -1,19 +1,9 @@ -"""App Page Object 基类 — 封装移动端页面共享的通用操作。 - -每个 App 页面应继承 AppBasePage,在 __init__ 中声明所有定位器。 - -Usage: - class DriverHomePage(AppBasePage): - def __init__(self, driver, platform): - super().__init__(driver, platform) - self.order_tab = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "tab_orders") -""" +"""通用 App Page Object 基类。""" from datetime import datetime from pathlib import Path from appium.webdriver.common.appiumby import AppiumBy -from appium.webdriver.webdriver import WebDriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC @@ -21,42 +11,33 @@ SCREENSHOTS_DIR = Path("output/screenshots") class AppBasePage: - """App Page Object 基类。""" + """App Page Object 基类 — 各端通用。""" - def __init__(self, driver: WebDriver, platform: str, timeout_ms: int = 30_000): + def __init__(self, driver, platform: str, timeout_ms: int = 30_000): self.driver = driver - self.platform = platform # "android" | "ios" - self.timeout = timeout_ms / 1000 # WebDriverWait 使用秒 - - # ── 通用等待 ── + self.platform = platform # "android" | "ios" + self.timeout = timeout_ms / 1000 def wait_visible_by_id(self, accessibility_id: str, timeout_s: float | None = None) -> None: - """等待 accessibility_id 对应的元素可见。""" wait = WebDriverWait(self.driver, timeout=timeout_s or self.timeout) wait.until( EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, accessibility_id)) ) def wait_visible_by_text(self, text: str, timeout_s: float | None = None) -> None: - """等待页面上出现指定文本。""" wait = WebDriverWait(self.driver, timeout=timeout_s or self.timeout) - locator = ( - AppiumBy.XPATH, f"//*[@text='{text}']" + (AppiumBy.XPATH, f"//*[@text='{text}']") if self.platform == "android" else (AppiumBy.XPATH, f"//*[@label='{text}']") ) wait.until(EC.visibility_of_element_located(locator)) - # ── 通用操作 ── - def tap_by_id(self, accessibility_id: str) -> None: - """按 accessibility_id 点击元素(推荐优先使用)。""" self.wait_visible_by_id(accessibility_id) self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id).click() def tap_by_text(self, text: str) -> None: - """按文本点击元素。""" strategy = AppiumBy.XPATH locator = ( f"//*[@text='{text}']" @@ -68,14 +49,12 @@ class AppBasePage: self.driver.find_element(strategy, locator).click() def fill_by_id(self, accessibility_id: str, value: str) -> None: - """按 accessibility_id 找到输入框并填入内容。""" self.wait_visible_by_id(accessibility_id) element = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id) element.clear() element.send_keys(value) def scroll_down(self) -> None: - """向下滑动(从屏幕 70% 滑到 30%)。""" size = self.driver.get_window_size() self.driver.swipe( start_x=size["width"] // 2, @@ -85,10 +64,7 @@ class AppBasePage: duration=500, ) - # ── 截图 ── - def screenshot(self, name: str) -> str: - """关键节点截图,返回文件路径。""" scenario_dir = SCREENSHOTS_DIR / self.__class__.__name__ scenario_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") diff --git a/tests/app/_shared.py b/tests/app/_shared.py new file mode 100644 index 0000000..03e7762 --- /dev/null +++ b/tests/app/_shared.py @@ -0,0 +1,125 @@ +"""App 端公共模块 — driver 创建 + ensure_logged_in 工厂。 + +所有 App 端 conftest 调用 share_* 函数即可,避免重复代码。 +""" + +from datetime import datetime +from pathlib import Path + +import pytest + +SCREENSHOTS_DIR = Path("output/screenshots") + + +def create_driver(section_config: dict, platform: str): + """创建 Appium driver。 + + Args: + section_config: 配置中对应端的 section(如 config["driver_android"]) + platform: "android" | "ios" + """ + try: + from appium import webdriver + from appium.options.android import UiAutomator2Options + from appium.options.ios import XCUITestOptions + except ImportError: + pytest.skip("Appium-Python-Client 未安装,跳过 App 测试。") + + appium_host = section_config.get("appium_host", "http://localhost:4723") + + if platform == "android": + options = UiAutomator2Options() + options.device_name = section_config["device_name"] + options.app_package = section_config["app_package"] + options.app_activity = section_config["app_activity"] + options.no_reset = section_config.get("no_reset", True) + options.new_command_timeout = section_config.get("new_command_timeout", 120) + options.automation_name = "UiAutomator2" + else: + options = XCUITestOptions() + options.device_name = section_config["device_name"] + options.bundle_id = section_config["bundle_id"] + options.no_reset = section_config.get("no_reset", True) + options.new_command_timeout = section_config.get("new_command_timeout", 120) + options.automation_name = "XCUITest" + + return webdriver.Remote(appium_host, options=options) + + +def make_ensure_logged_in( + section_config: dict, + config: dict, + account_role: str, + home_indicator: str = "tab_home", +): + """创建一个 ensure_logged_in 闭包。 + + Args: + section_config: App 端配置 + config: 全局配置 + account_role: 默认账号角色名(如 "driver"、"shipper") + home_indicator: 首页元素的 accessibility_id + """ + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions as EC + from appium.webdriver.common.appiumby import AppiumBy + + def _ensure_logged_in(driver, timeout_s: float = 10.0) -> None: + skip_login = section_config.get("skip_login", True) + wait = WebDriverWait(driver, timeout=timeout_s) + + try: + wait.until( + EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, home_indicator)) + ) + return + except Exception: + pass + + try: + login_input = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_phone_input") + if login_input.is_displayed(): + if skip_login: + raise RuntimeError( + "APP 在登录页,未检测到登录态!请在设备上手动完成一次登录(含验证码)," + "之后重新运行用例即可。\n如需自动化登录,请设置 skip_login: false。" + ) + account = config["accounts"].get(account_role, {}) + login_input.send_keys(account.get("phone", "")) + driver.find_element( + AppiumBy.ACCESSIBILITY_ID, "login_password_input" + ).send_keys(account.get("password", "")) + driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_submit_btn").click() + wait.until( + EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, home_indicator)) + ) + return + except RuntimeError: + raise + except Exception: + pass + + raise RuntimeError("无法确定 APP 的登录状态!请确保 APP 已启动且首页或登录页可见。") + + return _ensure_logged_in + + +def save_failure_screenshot(driver, section_config, platform, request): + """失败自动截图(各端 conftest teardown 中调用)。""" + if ( + section_config.get("screenshot_on_failure", True) + and hasattr(request.node, "rep_call") + and request.node.rep_call.failed + ): + scenario_dir = SCREENSHOTS_DIR / request.node.name + scenario_dir.mkdir(parents=True, exist_ok=True) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + path = str(scenario_dir / f"FAIL_{platform}_{ts}.png") + driver.save_screenshot(path) + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + rep = outcome.get_result() + setattr(item, f"rep_{rep.when}", rep) diff --git a/tests/app/conftest.py b/tests/app/conftest.py deleted file mode 100644 index 2ae4442..0000000 --- a/tests/app/conftest.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -App 端根级 fixture:Appium Driver 实例 + 登录态复用 + 失败自动截图。 - -核心机制 — 手动登录后复用: - Appium noReset=true 确保 app 数据不重置,登录态持久保留在设备上。 - 只需手动在设备上登录一次,后续用例通过 ensure_logged_in 检查首页是否已登录态。 - 如果检测到未登录(在登录页),给出明确指引让用户先去设备上手动登录。 - -配置 app.skip_login: true 时: - - 用例通过 ensure_logged_in() 检查是否已登录 - - 未登录则抛出明确错误,引导用户先去设备上手动登录 - - 已登录则直接开始执行 - -配置 app.skip_login: false 时: - - 每次用例尝试完整 UI 登录(有验证码时会失败) - -提供: - app_config — session 级 App 配置 - driver — Appium WebDriver(每个用例独立,失败自动截图) - platform — 当前平台 -""" - -from datetime import datetime -from pathlib import Path - -import pytest - -SCREENSHOTS_DIR = Path("output/screenshots") - - -@pytest.fixture(scope="session") -def app_config(config): - """session 级:App 配置段。""" - return config["app"] - - -@pytest.fixture -def platform(app_config) -> str: - """当前执行的平台。""" - return app_config.get("platforms", ["android"])[0] - - -@pytest.fixture -def driver(app_config, platform, request): - """每个用例独立的 Appium driver,失败自动截图。 - - 前提: Appium Server 已启动(appium --log-level info)。 - noReset=true → app 不重置 → 登录态持久保留。 - """ - try: - from appium import webdriver - from appium.options.android import UiAutomator2Options - from appium.options.ios import XCUITestOptions - except ImportError: - pytest.skip("Appium-Python-Client 未安装,跳过 App 测试。") - - appium_host = app_config.get("appium_host", "http://localhost:4723") - screenshot_on_failure = app_config.get("screenshot_on_failure", True) - - driver = None - try: - if platform == "android": - android_cfg = app_config["android"] - options = UiAutomator2Options() - options.device_name = android_cfg["device_name"] - options.app_package = android_cfg["app_package"] - options.app_activity = android_cfg["app_activity"] - options.no_reset = android_cfg.get("no_reset", True) - options.new_command_timeout = android_cfg.get("new_command_timeout", 120) - options.automation_name = "UiAutomator2" - else: - ios_cfg = app_config["ios"] - options = XCUITestOptions() - options.device_name = ios_cfg["device_name"] - options.bundle_id = ios_cfg["bundle_id"] - options.no_reset = ios_cfg.get("no_reset", True) - options.new_command_timeout = ios_cfg.get("new_command_timeout", 120) - options.automation_name = "XCUITest" - - driver = webdriver.Remote(appium_host, options=options) - - yield driver - - # ── 失败自动截图 ── - if ( - screenshot_on_failure - and hasattr(request.node, "rep_call") - and request.node.rep_call.failed - and driver is not None - ): - scenario_dir = SCREENSHOTS_DIR / request.node.name - scenario_dir.mkdir(parents=True, exist_ok=True) - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - path = str(scenario_dir / f"FAIL_{platform}_{ts}.png") - driver.save_screenshot(path) - - finally: - if driver is not None: - driver.quit() - - -@pytest.fixture -def ensure_logged_in(driver, platform, app_config, config): - """确保 APP 已处于登录状态。 - - 使用方式: - def test_xxx(driver, ensure_logged_in): - ensure_logged_in() # 检查登录态,未登录则报错 - # 后面直接写业务断言 - """ - - def _ensure_logged_in(timeout_s: float = 10.0) -> None: - skip_login = app_config.get("skip_login", True) - - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.support import expected_conditions as EC - from appium.webdriver.common.appiumby import AppiumBy - - # ⚠️ home_tab 的 accessibility_id 需根据实际 APP UI 确认 - HOME_INDICATOR_ID = "tab_home" # 首页 tab 的 accessibility_id - - wait = WebDriverWait(driver, timeout=timeout_s) - - try: - wait.until( - EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, HOME_INDICATOR_ID)) - ) - # 已登录,无需操作 - return - except Exception: - pass - - # ── 未检测到首页元素,判断是否在登录页 ── - try: - # 尝试找登录页元素 - login_input = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_phone_input") - if login_input.is_displayed(): - if skip_login: - raise RuntimeError( - "APP 当前在登录页,未检测到登录态!\n" - "请在设备上手动完成一次登录(包含验证码),之后重新运行用例即可。\n" - "如需通过自动化登录,请设置 app.skip_login: false(不推荐,有验证码)。" - ) - # skip_login: false — 走自动化登录 - account = config["accounts"].get("driver") - if not account: - raise RuntimeError("配置中未找到 driver 账号") - - login_input.send_keys(account["phone"]) - driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_password_input").send_keys( - account["password"] - ) - driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_submit_btn").click() - - # 等待登录完成 - wait.until( - EC.visibility_of_element_located( - (AppiumBy.ACCESSIBILITY_ID, HOME_INDICATOR_ID) - ) - ) - return - except RuntimeError: - raise # 重新抛出明确的错误提示 - except Exception: - pass - - raise RuntimeError( - f"无法确定 APP 的登录状态!请确保 APP 已启动且首页或登录页可见。\n" - f"当前平台: {platform}" - ) - - return _ensure_logged_in - - -@pytest.fixture -def is_logged_in(driver, platform, app_config) -> bool: - """快速检查当前是否已登录(不执行登录操作)。""" - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.support import expected_conditions as EC - from appium.webdriver.common.appiumby import AppiumBy - - try: - wait = WebDriverWait(driver, timeout=5) - wait.until( - EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "tab_home")) - ) - return True - except Exception: - return False - - -# ── pytest 报告 hook ── -@pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_makereport(item, call): - """记录用例执行结果,供失败截图使用。""" - outcome = yield - rep = outcome.get_result() - setattr(item, f"rep_{rep.when}", rep) diff --git a/tests/app/driver_android/conftest.py b/tests/app/driver_android/conftest.py new file mode 100644 index 0000000..a61eb60 --- /dev/null +++ b/tests/app/driver_android/conftest.py @@ -0,0 +1,27 @@ +"""司机安卓端 fixture — Appium + 登录态复用。""" + +import pytest +from tests.app._shared import create_driver, make_ensure_logged_in, save_failure_screenshot + +from tests.app._shared import pytest_runtest_makereport # noqa: F401 — report hook + + +@pytest.fixture +def driver(driver_android_config, request): + """司机安卓端 driver (noReset=true)。""" + cfg = driver_android_config + d = None + try: + d = create_driver(cfg, platform="android") + yield d + save_failure_screenshot(d, cfg, "android", request) + finally: + if d is not None: + d.quit() + + +@pytest.fixture +def ensure_logged_in(driver_android_config, config): + """确保已登录态(司机角色)。""" + fn = make_ensure_logged_in(driver_android_config, config, account_role="driver") + return lambda d: fn(d) diff --git a/tests/app/driver_android/pages/driver_home_page.py b/tests/app/driver_android/pages/driver_home_page.py new file mode 100644 index 0000000..c78476c --- /dev/null +++ b/tests/app/driver_android/pages/driver_home_page.py @@ -0,0 +1,26 @@ +"""司机安卓端 Page Object — 首页。""" + +from tests.app._base_page import AppBasePage + + +class DriverHomePage(AppBasePage): + """司机端 APP 首页。""" + + def __init__(self, driver, platform: str = "android"): + super().__init__(driver, platform) + self.order_tab = "tab_orders" + self.mine_tab = "tab_mine" + self.home_tab = "tab_home" + + def is_displayed(self) -> bool: + try: + self.wait_visible_by_id(self.home_tab, timeout_s=10) + return True + except Exception: + return False + + def go_to_order_list(self) -> None: + self.tap_by_id(self.order_tab) + + def go_to_mine(self) -> None: + self.tap_by_id(self.mine_tab) diff --git a/tests/app/driver_android/tests/test_smoke.py b/tests/app/driver_android/tests/test_smoke.py new file mode 100644 index 0000000..f8157ff --- /dev/null +++ b/tests/app/driver_android/tests/test_smoke.py @@ -0,0 +1,18 @@ +"""司机安卓端 — 冒烟测试。""" + +import pytest + +pytestmark = [pytest.mark.driver_android] + + +class TestDriverAndroidSmoke: + """司机安卓端基础验证。""" + + @pytest.mark.p0 + @pytest.mark.smoke + def test_home_page_loaded(self, driver, ensure_logged_in): + """验证登录态有效,首页正常。""" + ensure_logged_in(driver) + from appium.webdriver.common.appiumby import AppiumBy + home = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "tab_home") + assert home.is_displayed(), "首页 tab 应可见" diff --git a/tests/app/driver_ios/conftest.py b/tests/app/driver_ios/conftest.py new file mode 100644 index 0000000..341ec56 --- /dev/null +++ b/tests/app/driver_ios/conftest.py @@ -0,0 +1,27 @@ +"""司机苹果端 fixture — Appium + 登录态复用。""" + +import pytest +from tests.app._shared import create_driver, make_ensure_logged_in, save_failure_screenshot + +from tests.app._shared import pytest_runtest_makereport # noqa: F401 + + +@pytest.fixture +def driver(driver_ios_config, request): + """司机苹果端 driver。""" + cfg = driver_ios_config + d = None + try: + d = create_driver(cfg, platform="ios") + yield d + save_failure_screenshot(d, cfg, "ios", request) + finally: + if d is not None: + d.quit() + + +@pytest.fixture +def ensure_logged_in(driver_ios_config, config): + """确保已登录态(司机角色)。""" + fn = make_ensure_logged_in(driver_ios_config, config, account_role="driver") + return lambda d: fn(d) diff --git a/tests/app/driver_ios/pages/__init__.py b/tests/app/driver_ios/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/driver_ios/tests/__init__.py b/tests/app/driver_ios/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/driver_ios/tests/test_smoke.py b/tests/app/driver_ios/tests/test_smoke.py new file mode 100644 index 0000000..e94cb61 --- /dev/null +++ b/tests/app/driver_ios/tests/test_smoke.py @@ -0,0 +1,18 @@ +"""司机苹果端 — 冒烟测试。""" + +import pytest + +pytestmark = [pytest.mark.driver_ios] + + +class TestDriverIosSmoke: + """司机苹果端基础验证。""" + + @pytest.mark.p0 + @pytest.mark.smoke + def test_home_page_loaded(self, driver, ensure_logged_in): + """验证登录态有效,首页正常。""" + ensure_logged_in(driver) + from appium.webdriver.common.appiumby import AppiumBy + home = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "tab_home") + assert home.is_displayed(), "首页 tab 应可见" diff --git a/tests/app/pages/driver_home_page.py b/tests/app/pages/driver_home_page.py deleted file mode 100644 index 284086c..0000000 --- a/tests/app/pages/driver_home_page.py +++ /dev/null @@ -1,47 +0,0 @@ -"""司机端首页 — Page Object(App 端)。 - -Usage: - from tests.app.pages.driver_home_page import DriverHomePage - - home = DriverHomePage(driver, platform="android") - home.go_to_order_list() -""" - -from appium.webdriver.webdriver import WebDriver - -from tests.app.pages.base_page import AppBasePage - - -class DriverHomePage(AppBasePage): - """运八司机端 APP 首页。""" - - def __init__(self, driver: WebDriver, platform: str = "android"): - super().__init__(driver, platform) - # ⚠️ 以下 accessibility_id 需要根据实际 APP UI 确认 - self.order_tab = "tab_orders" # 运单 tab - self.mine_tab = "tab_mine" # 我的 tab - self.home_tab = "tab_home" # 首页 tab - self.notification_badge = "badge_notification" - - def is_displayed(self) -> bool: - """检查首页是否已展示。""" - try: - self.wait_visible_by_id(self.home_tab, timeout_s=10) - return True - except Exception: - return False - - def go_to_order_list(self) -> None: - """点击进入运单列表。""" - self.tap_by_id(self.order_tab) - - def go_to_mine(self) -> None: - """点击进入「我的」页面。""" - self.tap_by_id(self.mine_tab) - - def get_notification_count(self) -> str: - """获取通知角标数量。""" - element = self.driver.find_element( - self.driver.find_element.ACCESSIBILITY_ID, self.notification_badge - ) - return element.text if element else "0" diff --git a/tests/app/shipper_android/conftest.py b/tests/app/shipper_android/conftest.py new file mode 100644 index 0000000..80a8e9d --- /dev/null +++ b/tests/app/shipper_android/conftest.py @@ -0,0 +1,27 @@ +"""货主安卓端 fixture — Appium + 登录态复用。""" + +import pytest +from tests.app._shared import create_driver, make_ensure_logged_in, save_failure_screenshot + +from tests.app._shared import pytest_runtest_makereport # noqa: F401 + + +@pytest.fixture +def driver(shipper_android_config, request): + """货主安卓端 driver。""" + cfg = shipper_android_config + d = None + try: + d = create_driver(cfg, platform="android") + yield d + save_failure_screenshot(d, cfg, "android", request) + finally: + if d is not None: + d.quit() + + +@pytest.fixture +def ensure_logged_in(shipper_android_config, config): + """确保已登录态(货主角色)。""" + fn = make_ensure_logged_in(shipper_android_config, config, account_role="shipper") + return lambda d: fn(d) diff --git a/tests/app/shipper_android/pages/__init__.py b/tests/app/shipper_android/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/shipper_android/tests/__init__.py b/tests/app/shipper_android/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/shipper_android/tests/test_smoke.py b/tests/app/shipper_android/tests/test_smoke.py new file mode 100644 index 0000000..fff8953 --- /dev/null +++ b/tests/app/shipper_android/tests/test_smoke.py @@ -0,0 +1,18 @@ +"""货主安卓端 — 冒烟测试。""" + +import pytest + +pytestmark = [pytest.mark.shipper_android] + + +class TestShipperAndroidSmoke: + """货主安卓端基础验证。""" + + @pytest.mark.p0 + @pytest.mark.smoke + def test_home_page_loaded(self, driver, ensure_logged_in): + """验证登录态有效,首页正常。""" + ensure_logged_in(driver) + from appium.webdriver.common.appiumby import AppiumBy + home = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "tab_home") + assert home.is_displayed(), "首页 tab 应可见" diff --git a/tests/app/shipper_ios/conftest.py b/tests/app/shipper_ios/conftest.py new file mode 100644 index 0000000..5bea314 --- /dev/null +++ b/tests/app/shipper_ios/conftest.py @@ -0,0 +1,27 @@ +"""货主苹果端 fixture — Appium + 登录态复用。""" + +import pytest +from tests.app._shared import create_driver, make_ensure_logged_in, save_failure_screenshot + +from tests.app._shared import pytest_runtest_makereport # noqa: F401 + + +@pytest.fixture +def driver(shipper_ios_config, request): + """货主苹果端 driver。""" + cfg = shipper_ios_config + d = None + try: + d = create_driver(cfg, platform="ios") + yield d + save_failure_screenshot(d, cfg, "ios", request) + finally: + if d is not None: + d.quit() + + +@pytest.fixture +def ensure_logged_in(shipper_ios_config, config): + """确保已登录态(货主角色)。""" + fn = make_ensure_logged_in(shipper_ios_config, config, account_role="shipper") + return lambda d: fn(d) diff --git a/tests/app/shipper_ios/pages/__init__.py b/tests/app/shipper_ios/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/shipper_ios/tests/__init__.py b/tests/app/shipper_ios/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/app/shipper_ios/tests/test_smoke.py b/tests/app/shipper_ios/tests/test_smoke.py new file mode 100644 index 0000000..94c7f71 --- /dev/null +++ b/tests/app/shipper_ios/tests/test_smoke.py @@ -0,0 +1,18 @@ +"""货主苹果端 — 冒烟测试。""" + +import pytest + +pytestmark = [pytest.mark.shipper_ios] + + +class TestShipperIosSmoke: + """货主苹果端基础验证。""" + + @pytest.mark.p0 + @pytest.mark.smoke + def test_home_page_loaded(self, driver, ensure_logged_in): + """验证登录态有效,首页正常。""" + ensure_logged_in(driver) + from appium.webdriver.common.appiumby import AppiumBy + home = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "tab_home") + assert home.is_displayed(), "首页 tab 应可见" diff --git a/tests/app/test_login/test_login.py b/tests/app/test_login/test_login.py deleted file mode 100644 index 57cbb71..0000000 --- a/tests/app/test_login/test_login.py +++ /dev/null @@ -1,108 +0,0 @@ -"""App 登录功能测试用例。 - -覆盖: - - (skip_login: true) 验证预登录态有效,用例直接复用已登录状态 - - (skip_login: false) 完整 UI 登录流程 - -前置条件: - 1. Appium Server 已启动: appium --log-level info - 2. (推荐) 已在设备上手动登录一次,后续用例自动跳过登录 - 3. app.skip_login: true(默认)→ 用例通过 ensure_logged_in 检查登录态 - 4. app.skip_login: false → 每次执行完整 UI 登录(有验证码会失败) - -标记: - pytest -m app # 仅执行 App 用例 - pytest -m p0 # 仅执行 P0 冒烟 - pytest -m smoke # 仅执行冒烟 -""" - -import pytest -from appium.webdriver.common.appiumby import AppiumBy - -pytestmark = [pytest.mark.app] - - -# ═══════════════════════════════════════════════════════════════════ -# P0 — 冒烟测试(依赖手动登录后的持久化登录态) -# ═══════════════════════════════════════════════════════════════════ - -class TestAppSmoke: - """验证 App 登录态有效,核心页面可正常访问。""" - - @pytest.mark.p0 - @pytest.mark.smoke - def test_already_logged_in(self, driver, platform, ensure_logged_in): - """验证 APP 已处于登录状态(手动登录后 noReset 持久化)。""" - ensure_logged_in() - # 主页面 tab 应可见 - home_tab = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "tab_home") - assert home_tab.is_displayed(), "登录后应展示首页 tab" - - @pytest.mark.p0 - @pytest.mark.smoke - def test_navigate_to_order_list(self, driver, platform, ensure_logged_in): - """验证登录后可导航到运单列表。""" - from tests.app.pages.driver_home_page import DriverHomePage - - ensure_logged_in() - home = DriverHomePage(driver, platform=platform) - home.go_to_order_list() - - # 验证运单列表可见 - # ⚠️ 定位需根据实际 APP UI 调整 - order_list = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "order_list") - assert order_list.is_displayed(), "运单列表应可见" - - @pytest.mark.p0 - @pytest.mark.smoke - def test_tab_switching(self, driver, platform, ensure_logged_in): - """验证底部 tab 可正常切换。""" - from tests.app.pages.driver_home_page import DriverHomePage - - ensure_logged_in() - home = DriverHomePage(driver, platform=platform) - - # 点击"我的" - home.go_to_mine() - # ⚠️ mine_page 的 accessibility_id 需根据实际 APP UI 确认 - mine_view = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "mine_page") - assert mine_view.is_displayed(), "应展示「我的」页面" - - # 切回首页 - home.tap_by_id("tab_home") - assert home.is_displayed(), "应切回首页" - - -# ═══════════════════════════════════════════════════════════════════ -# P1 — 登录页验证(仅在 skip_login: false 时有意义) -# ═══════════════════════════════════════════════════════════════════ - -class TestAppLoginPage: - """登录页基础验证。""" - - @pytest.mark.p1 - def test_login_page_elements(self, driver, platform, app_config): - """验证登录页元素存在(不执行登录)。""" - # ⚠️ 前提: app 当前在登录页(未登录状态) - # 如果已登录,此用例可能找不到登录页元素 - from tests.app.conftest import is_logged_in - - # 如果已登录,跳过此用例 - try: - if is_logged_in: - pytest.skip("APP 已登录,无法验证登录页元素") - except Exception: - pass - - # 验证登录页核心元素 - try: - phone_input = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_phone_input") - assert phone_input.is_displayed(), "手机号输入框应可见" - - password_input = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_password_input") - assert password_input.is_displayed(), "密码输入框应可见" - - login_btn = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_submit_btn") - assert login_btn.is_displayed(), "登录按钮应可见" - except Exception: - pytest.skip("登录页元素未找到,APP 可能已登录或 UI 已变更") diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 0cef662..ea1ba03 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -6,8 +6,12 @@ Usage: from tests.config.test_config import load_config config = load_config() - web_url = config["web"]["base_url"] + admin_url = config["admin"]["base_url"] admin_phone = config["accounts"]["super_admin"]["phone"] + +各端通过 fixture 直接引用对应 section: + def test_xxx(admin_config): + page.goto(admin_config["base_url"]) """ from pathlib import Path diff --git a/tests/config/test_config.yaml b/tests/config/test_config.yaml index 1589f65..3dc741b 100644 --- a/tests/config/test_config.yaml +++ b/tests/config/test_config.yaml @@ -1,9 +1,16 @@ # 运八网络货运平台 — 测试环境配置 # 数据来源: knowledge_base/00_project/project_profile.md -# 使用方式: from config.test_config import load_config +# 使用方式: from tests.config.test_config import load_config +# +# 覆盖端: +# Web: 管理端 admin / 司机端小程序 mini_program +# App: 司机安卓 driver_android / 货主安卓 shipper_android +# 司机苹果 driver_ios / 货主苹果 shipper_ios -# ── Web 管理端 ── -web: +# ═══════════════════════════════════════════════════════════════════ +# ── 管理端 (Web/Playwright) ── +# ═══════════════════════════════════════════════════════════════════ +admin: base_url: "https://ybxcx.ynyun8.com:8000/admin" api_base_url: "https://ybxcx.ynyun8.com:8000/" timeout_seconds: 120 @@ -12,71 +19,127 @@ web: browsers: - chromium - firefox - # WebKit 默认关闭(Windows 下问题较多),CI 环境可开启 viewport: width: 1920 height: 1080 - # 登录态复用(跳过图形验证码) - # storage_state 文件保存了浏览器登录后的 cookies + localStorage - # 首次使用需手动登录一次生成,流程: - # 1. 运行: python tests/web/save_auth.py - # 2. 脚本打开浏览器 → 人工完成验证码登录 → 登录态自动保存 - # 3. 后续执行用例时自动复用,无需重复登录 - # - # skip_login: true → 加载已有的 storage_state,跳过登录步骤 - # skip_login: false → 每次用例都通过 UI 执行完整登录流程 + # 登录态复用: python tests/web/admin/save_auth.py 生成后自动跳过验证码 skip_login: true - storage_state_dir: "tests/web/auth" # 登录态存储目录 + storage_state_dir: "tests/web/auth" -# ── App 移动端 ── -app: +# ═══════════════════════════════════════════════════════════════════ +# ── 司机端小程序 (Web/Playwright,模拟微信 WebView) ── +# ═══════════════════════════════════════════════════════════════════ +mini_program: + base_url: "https://ybxcx.ynyun8.com:8000/mini" # ⚠️ 请确认小程序 WebView 实际地址 + timeout_seconds: 120 + screenshot_on_failure: true + screenshot_on_step: false + browsers: + - chromium + # 模拟微信内置浏览器 UA + user_agent: "Mozilla/5.0 MicroMessenger/8.0.0" + viewport: + width: 375 + height: 812 + + # 小程序本质是 WebView,同样支持 storage_state 复用 + skip_login: true + storage_state_dir: "tests/web/auth" + +# ═══════════════════════════════════════════════════════════════════ +# ── 司机安卓端 (App/Appium) ── +# ═══════════════════════════════════════════════════════════════════ +driver_android: appium_host: "http://localhost:4723" timeout_seconds: 120 screenshot_on_failure: true - platforms: - - android - # - ios # iOS 仅在 macOS CI 环境下开启 + platform: android + skip_login: true # 手动登录一次后 noReset 自动复用 - # 登录态复用(跳过图形验证码) - # Appium noReset=true 已确保 app 数据不重置,登录态持久保留 - # 使用方式:手动启动 app 登录一次 → 后续用例自动跳过登录 - # - # skip_login: true → 用例直接检查首页是否已登录态,跳过登录步骤 - # skip_login: false → 每次用例都执行完整 UI 登录流程 + device_name: "Android Emulator" + app_package: "com.arpa.ynchenggangdriver" # ⚠️ 请确认实际包名 + app_activity: "com.arpa.ntocc.MainActivity" # ⚠️ 请确认实际启动 Activity + no_reset: true + new_command_timeout: 120 + +# ═══════════════════════════════════════════════════════════════════ +# ── 货主安卓端 (App/Appium) ── +# ═══════════════════════════════════════════════════════════════════ +shipper_android: + appium_host: "http://localhost:4723" + timeout_seconds: 120 + screenshot_on_failure: true + platform: android skip_login: true - android: - device_name: "Android Emulator" - app_package: "com.yunba.driver" # ⚠️ 请确认实际包名 - app_activity: ".MainActivity" # ⚠️ 请确认实际启动 Activity - no_reset: true - new_command_timeout: 120 + device_name: "Android Emulator" + app_package: "com.yunba.shipper" # ⚠️ 请确认实际包名 + app_activity: ".MainActivity" # ⚠️ 请确认实际启动 Activity + no_reset: true + new_command_timeout: 120 - ios: - device_name: "iPhone 15" - bundle_id: "com.yunba.driver" # ⚠️ 请确认实际 Bundle ID - no_reset: true - new_command_timeout: 120 +# ═══════════════════════════════════════════════════════════════════ +# ── 司机苹果端 (App/Appium,需 macOS) ── +# ═══════════════════════════════════════════════════════════════════ +driver_ios: + appium_host: "http://localhost:4723" + timeout_seconds: 120 + screenshot_on_failure: true + platform: ios + skip_login: true + device_name: "iPhone 15" + bundle_id: "com.arpa.ynchenggangdriver" # ⚠️ 请确认实际 Bundle ID + no_reset: true + new_command_timeout: 120 + +# ═══════════════════════════════════════════════════════════════════ +# ── 货主苹果端 (App/Appium,需 macOS) ── +# ═══════════════════════════════════════════════════════════════════ +shipper_ios: + appium_host: "http://localhost:4723" + timeout_seconds: 120 + screenshot_on_failure: true + platform: ios + skip_login: true + + device_name: "iPhone 15" + bundle_id: "com.yunba.shipper" # ⚠️ 请确认实际 Bundle ID + no_reset: true + new_command_timeout: 120 + +# ═══════════════════════════════════════════════════════════════════ # ── 测试账号 ── +# ═══════════════════════════════════════════════════════════════════ accounts: super_admin: phone: "super_admin" - password: "951260684NiAn.." # ⚠️ 若密码变更请同步修改 + password: "951260684NiAn.." role: "平台运营人员" + use_on: [admin] team_leader: phone: "13113113113" password: "88888888" role: "车队长" + use_on: [admin, mini_program] driver: phone: "15188888888" password: "88888888" role: "司机" + use_on: [mini_program, driver_android, driver_ios] + shipper: + phone: "13800000000" # ⚠️ 请确认实际货主账号 + password: "88888888" + role: "货主" + use_on: [shipper_android, shipper_ios] + +# ═══════════════════════════════════════════════════════════════════ # ── 报告 ── +# ═══════════════════════════════════════════════════════════════════ report: logs_dir: "output/logs" screenshots_dir: "output/screenshots" diff --git a/tests/conftest.py b/tests/conftest.py index bf917c6..757af34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,27 +1,63 @@ -"""根级 conftest — 全局 fixture:配置加载、日志、测试数据清理。 +"""根级 conftest — 全局 fixture:配置加载、日志。 -自动应用于 tests/ 下所有测试模块。 +Usage: + def test_xxx(admin_config): # 管理端配置 + def test_xxx(driver_android_config): # 司机安卓配置 """ from pathlib import Path +import sys import pytest -# 将 tests/ 目录加入 Python path,确保 `from tests.xxx import yyy` 可用 -import sys sys.path.insert(0, str(Path(__file__).parent.parent)) @pytest.fixture(scope="session") def config(): - """全局配置 fixture(session 级别,只加载一次)。""" + """全局配置(session 级,只加载一次)。""" from tests.config.test_config import load_config return load_config() @pytest.fixture(scope="function") def logger(request): - """每个测试函数一个 logger,自动以模块名命名。""" + """每个测试函数的 logger。""" from tests.utils.logger import get_logger module_name = request.module.__name__.replace("tests.", "") return get_logger(module_name) + + +# ═══════════════════════════════════════════════════════════════════ +# 各端配置 fixture(从 config 中摘取对应 section) +# ═══════════════════════════════════════════════════════════════════ + +@pytest.fixture(scope="session") +def admin_config(config): + """管理端 (Playwright) 配置。""" + return config["admin"] + +@pytest.fixture(scope="session") +def mini_program_config(config): + """司机端小程序 (Playwright) 配置。""" + return config["mini_program"] + +@pytest.fixture(scope="session") +def driver_android_config(config): + """司机安卓端 (Appium) 配置。""" + return config["driver_android"] + +@pytest.fixture(scope="session") +def shipper_android_config(config): + """货主安卓端 (Appium) 配置。""" + return config["shipper_android"] + +@pytest.fixture(scope="session") +def driver_ios_config(config): + """司机苹果端 (Appium) 配置。""" + return config["driver_ios"] + +@pytest.fixture(scope="session") +def shipper_ios_config(config): + """货主苹果端 (Appium) 配置。""" + return config["shipper_ios"] diff --git a/tests/pytest.ini b/tests/pytest.ini index ce2abed..dbfc487 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -23,8 +23,14 @@ markers = p1: P1 主要功能 p2: P2 次要/异常流程 smoke: 冒烟测试 - web: Web UI 测试(Playwright) - app: App UI 测试(Appium) + admin: 管理端 (Playwright) + mini_program: 司机端小程序 (Playwright) + driver_android: 司机安卓端 (Appium) + shipper_android: 货主安卓端 (Appium) + driver_ios: 司机苹果端 (Appium) + shipper_ios: 货主苹果端 (Appium) + appium: 所有 Appium 测试 + playwright: 所有 Playwright 测试 # 日志格式 log_cli = true diff --git a/tests/web/admin/conftest.py b/tests/web/admin/conftest.py new file mode 100644 index 0000000..1ac10da --- /dev/null +++ b/tests/web/admin/conftest.py @@ -0,0 +1,126 @@ +"""管理端 fixture — Playwright 浏览器实例 + 登录态复用。 + +核心机制: + skip_login: true → 加载 storage_state,跳过验证码 + skip_login: false → 执行完整 UI 登录 + +初始化: + python tests/web/admin/save_auth.py +""" + +from datetime import datetime +from pathlib import Path + +import pytest +from playwright.sync_api import sync_playwright, Page + +SCREENSHOTS_DIR = Path("output/screenshots") + + +@pytest.fixture(params=["chromium", "firefox"]) +def browser_name(admin_config, request): + """每个浏览器执行一轮。""" + browsers = admin_config.get("browsers", ["chromium", "firefox"]) + request.param = browsers[len(getattr(request, "_index", 0))] + return request.param + + +@pytest.fixture +def page(admin_config, browser_name, request) -> Page: + """Playwright Page,加载登录态,失败自动截图。""" + viewport = admin_config.get("viewport", {"width": 1920, "height": 1080}) + timeout = admin_config.get("timeout_seconds", 120) * 1000 + skip_login = admin_config.get("skip_login", True) + storage_state_dir = Path(admin_config.get("storage_state_dir", "tests/web/auth")) + + with sync_playwright() as p: + browser_launcher = getattr(p, browser_name) + browser = browser_launcher.launch(headless=True) + + context_kwargs = { + "viewport": {"width": viewport["width"], "height": viewport["height"]}, + "locale": "zh-CN", + } + + loaded_role = None + if skip_login: + loaded_role = _load_auth_state(context_kwargs, storage_state_dir, browser_name) + + context = browser.new_context(**context_kwargs) + page = context.new_page() + page.set_default_timeout(timeout) + page._auth_role = loaded_role + + yield page + + if hasattr(request.node, "rep_call") and request.node.rep_call.failed: + scenario_dir = SCREENSHOTS_DIR / request.node.name + scenario_dir.mkdir(parents=True, exist_ok=True) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + path = str(scenario_dir / f"FAIL_{browser_name}_{ts}.png") + page.screenshot(path=path, full_page=True) + + context.close() + browser.close() + + +@pytest.fixture +def ensure_login(admin_config, page, config): + """确保管理端已登录。""" + + def _ensure_login(role: str = "super_admin") -> None: + account = config["accounts"].get(role) + if not account: + raise ValueError(f"未知角色 '{role}',可用: {list(config['accounts'].keys())}") + + base_url = admin_config["base_url"] + skip_login = admin_config.get("skip_login", True) + + if skip_login: + auth_role = getattr(page, "_auth_role", None) + if auth_role == role: + page.goto(base_url) + page.wait_for_timeout(2000) + if "#/login" not in page.url and "/login" not in page.url: + return + raise RuntimeError( + f"登录态已过期!请运行: python tests/web/admin/save_auth.py --role {role}" + ) + if auth_role and auth_role != role: + raise RuntimeError( + f"当前 storage_state 角色为 '{auth_role}',请求 '{role}'。\n" + f"请运行: python tests/web/admin/save_auth.py --role {role}" + ) + raise RuntimeError( + f"未找到角色 '{role}' 的登录态文件!请运行:\n" + f" python tests/web/admin/save_auth.py --role {role}" + ) + + # skip_login: false + page.goto(f"{base_url}/#/login") + page.get_by_placeholder("请输入用户名").fill(account["phone"]) + page.get_by_placeholder("请输入密码").fill(account["password"]) + page.get_by_role("button", name="登录").click() + page.wait_for_timeout(3000) + if "#/login" in page.url or "/login" in page.url: + raise RuntimeError( + "登录失败!可能存在图形验证码。请改为手动登录: python tests/web/admin/save_auth.py" + ) + + return _ensure_login + + +def _load_auth_state(context_kwargs: dict, storage_dir: Path, browser: str) -> str | None: + auth_files = list(storage_dir.glob(f"*_{browser}.json")) if storage_dir.exists() else [] + if not auth_files: + return None + auth_file = auth_files[0] + context_kwargs["storage_state"] = str(auth_file) + return auth_file.stem.replace(f"_{browser}", "") + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + rep = outcome.get_result() + setattr(item, f"rep_{rep.when}", rep) diff --git a/tests/web/pages/base_page.py b/tests/web/admin/pages/base_page.py similarity index 100% rename from tests/web/pages/base_page.py rename to tests/web/admin/pages/base_page.py diff --git a/tests/web/pages/components/navbar.py b/tests/web/admin/pages/components/navbar.py similarity index 92% rename from tests/web/pages/components/navbar.py rename to tests/web/admin/pages/components/navbar.py index 530c4a3..415e211 100644 --- a/tests/web/pages/components/navbar.py +++ b/tests/web/admin/pages/components/navbar.py @@ -3,7 +3,7 @@ 跨页面复用的顶部/侧边导航操作。 Usage: - from tests.web.pages.components.navbar import NavBar + from tests.web.admin.pages.components.navbar import NavBar navbar = NavBar(page) navbar.go_to("运输管理") @@ -11,7 +11,7 @@ Usage: from playwright.sync_api import Page, expect -from tests.web.pages.base_page import BasePage +from tests.web.admin.pages.base_page import BasePage class NavBar(BasePage): diff --git a/tests/web/pages/login_page.py b/tests/web/admin/pages/login_page.py similarity index 94% rename from tests/web/pages/login_page.py rename to tests/web/admin/pages/login_page.py index 58e9a32..6aa4283 100644 --- a/tests/web/pages/login_page.py +++ b/tests/web/admin/pages/login_page.py @@ -1,7 +1,7 @@ """登录页面 — Page Object。 Usage: - from tests.web.pages.login_page import LoginPage + from tests.web.admin.pages.login_page import LoginPage login_page = LoginPage(page) login_page.login(phone="super_admin", password="951260684NiAn..") @@ -9,7 +9,7 @@ Usage: from playwright.sync_api import Page, expect -from tests.web.pages.base_page import BasePage +from tests.web.admin.pages.base_page import BasePage class LoginPage(BasePage): diff --git a/tests/web/save_auth.py b/tests/web/admin/save_auth.py similarity index 95% rename from tests/web/save_auth.py rename to tests/web/admin/save_auth.py index 3349a4e..9e2b735 100644 --- a/tests/web/save_auth.py +++ b/tests/web/admin/save_auth.py @@ -5,7 +5,7 @@ cookies + localStorage 序列化到 JSON 文件,后续用例自动复用。 用法: - python tests/web/save_auth.py + python tests/web/admin/save_auth.py 流程: 1. 脚本打开浏览器到登录页 @@ -44,7 +44,7 @@ def main(): args = parser.parse_args() config = load_config() - web_config = config["web"] + web_config = config["admin"] base_url = web_config["base_url"] login_url = f"{base_url}/#/login" @@ -92,7 +92,7 @@ def main(): browser.close() - print(f"\n🎉 现在可以跑了: pytest tests/web/ -m p0") + print(f"\n🎉 现在可以跑了: pytest tests/web/admin/ -m p0") if __name__ == "__main__": diff --git a/tests/web/admin/test_login/test_login.py b/tests/web/admin/test_login/test_login.py new file mode 100644 index 0000000..6034538 --- /dev/null +++ b/tests/web/admin/test_login/test_login.py @@ -0,0 +1,74 @@ +"""管理端登录测试用例。 + +标记: + pytest -m admin # 仅管理端用例 + pytest -m p0 # P0 冒烟 + pytest -m smoke # 冒烟 +""" + +import pytest +from playwright.sync_api import Page + +pytestmark = [pytest.mark.admin] + + +class TestAdminSmoke: + """管理端冒烟 — 验证预登录态。""" + + @pytest.mark.p0 + @pytest.mark.smoke + def test_admin_access_with_saved_auth(self, page: Page, admin_config, ensure_login): + """验证预登录态可直接访问管理后台。""" + ensure_login("super_admin") + page.goto(admin_config["base_url"]) + page.wait_for_timeout(2000) + assert "#/login" not in page.url and "/login" not in page.url, ( + "预登录态失效!请运行: python tests/web/admin/save_auth.py" + ) + + @pytest.mark.p0 + @pytest.mark.smoke + def test_team_leader_access(self, page: Page, admin_config, ensure_login): + """验证车队长预登录态有效。""" + ensure_login("team_leader") + page.goto(admin_config["base_url"]) + page.wait_for_timeout(2000) + assert "#/login" not in page.url and "/login" not in page.url + + @pytest.mark.p0 + @pytest.mark.smoke + def test_dashboard_loads(self, page: Page, admin_config, ensure_login): + """验证首页可正常加载。""" + ensure_login("super_admin") + page.goto(admin_config["base_url"]) + assert page.title(), "首页应显示页面标题" + + +class TestLoginPage: + """登录页交互验证。""" + + @pytest.mark.p1 + def test_login_page_accessible(self, page: Page, admin_config): + """验证登录页可正常访问。""" + page.goto(f"{admin_config['base_url']}/#/login") + assert page.get_by_placeholder("请输入用户名").is_visible() + + @pytest.mark.p1 + def test_login_page_elements(self, page: Page, admin_config): + """验证登录页元素完整。""" + page.goto(f"{admin_config['base_url']}/#/login") + assert page.get_by_placeholder("请输入用户名").is_visible() + assert page.get_by_placeholder("请输入密码").is_visible() + assert page.get_by_role("button", name="登录").is_visible() + + @pytest.mark.p1 + def test_session_redirect(self, page: Page, admin_config, ensure_login): + """验证已登录用户访问登录页被重定向。""" + ensure_login("super_admin") + page.goto(admin_config["base_url"]) + page.wait_for_timeout(2000) + assert "#/login" not in page.url + + page.goto(f"{admin_config['base_url']}/#/login") + page.wait_for_timeout(2000) + assert "#/login" not in page.url and "/login" not in page.url diff --git a/tests/web/auth/super_admin_chromium.json b/tests/web/auth/super_admin_chromium.json new file mode 100644 index 0000000..26d6fd7 --- /dev/null +++ b/tests/web/auth/super_admin_chromium.json @@ -0,0 +1 @@ +{"cookies": [{"name": "GouldAMap", "value": "d58bb5f7da1aa1d6b78741ded2450f2d", "domain": "ybxcx.ynyun8.com", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "Admin-Token", "value": "d5cb7b227e9fb37e4e0f0caca00bxc39c18", "domain": "ybxcx.ynyun8.com", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax"}], "origins": [{"origin": "https://ybxcx.ynyun8.com:8000", "localStorage": [{"name": "_AMap_AMap.Geolocation", "value": "{\"version\":\"1768801478717\",\"script\":\"g.Vn=g.da.extend({ka:[g.va],Ni:\\\".amap-geolocation-con .amap-geo{background:#fff url(../../theme/v1.3/markers/b/loc_gray.png) 50% 50% no-repeat;width:35px;height:35px;border:1px solid #ccc;border-radius:3px;right:4px}.amap-locate-loading .amap-geo{background-image:url(../../theme/v1.3/loading.gif)}.amap-locate{position:absolute;width:18px;height:18px;background:url(../../theme/v1.3/map_view.png);_background:url(../../theme/v1.3/map_view.gif);background-position:-130px -185px;cursor:pointer}\\\",A:function(a){this.CLASS_NAME= \\\"AMap.Geolocation\\\";g.c.ya(this,a);this.options={enableHighAccuracy:!1,timeout:5E3,maximumAge:0,GeoLocationFirst:!1,convert:!0,convertUrl:g.o.fd+\\\"/v3/assistant/coordinate/convert?coordsys=gps&output=json&s=rsv3&locations={x},{y}&key=\\\"+g.o.key,showButton:!0,buttonDom:'
',buttonPosition:\\\"LB\\\",buttonOffset:new g.H(10,20),showMarker:!0,markerOptions:{innerOverlay:!0,offset:new g.H(-11,-11),content:\\\"
\\\"},showCircle:!0,circleOptions:{innerOverlay:!0,strokeColor:\\\"#0093FF\\\",noSelect:!0,strokeOpacity:0.5,strokeWeight:1,fillColor:\\\"#02B0FF\\\",fillOpacity:0.25},panToLocation:!0,zoomToAccuracy:!1,noIpLocate:0,noGeoLocation:0,useNative:!1,extensions:\\\"base\\\",stopWhenPermissionDenied:!0};if(a)for(var b in a)b in this.options&&(this.options[b]=a[b]);switch(this.options.noGeoLocation){case 0:this.kA=!0;break;case 1:this.kA=!g.l.ba;break;case 2:this.kA=g.l.ba;break;case 3:this.kA=!1;break; default:this.kA=!0}switch(this.options.noIpLocate){case 0:this.Ew=!0;break;case 1:this.Ew=!g.l.ba;break;case 2:this.Ew=g.l.ba;break;case 3:this.Ew=!1;break;case !0:this.Ew=!1;break;default:this.Ew=!0}this.Lb=this.Tb=this.MM=this.rx=this.hi=this.e=null;this.bY=g.event.addListener(this,\\\"complete\\\",this.Oua,this);this.uY=g.event.addListener(this,\\\"error\\\",this.Pua,this)},isSupported:function(){return!!navigator.geolocation},Ul:function(a){new g.kb.Ab(g.o.Cc+\\\"://webapi.amap.com/count?\\\"+[\\\"type=loc\\\",\\\"k=\\\"+ g.o.key,\\\"u=\\\"+g.o.Ao,\\\"m=\\\"+(g.l.ba?1:0),\\\"pf=\\\"+g.l.Fz,\\\"suc=\\\"+a].join(\\\"&\\\"))},Hqa:function(a){function b(b){b.status=1;d.Lv(b,function(b,c){c.info=\\\"SUCCESS\\\";a&&a(\\\"complete\\\",c);g.event.O(d,\\\"complete\\\",c)})}function c(b){function c(f){f.message=b.message+f.message;f.status=1;d.Lv(f,function(b,c){c.info=\\\"SUCCESS\\\";a&&a(\\\"complete\\\",c);g.event.O(d,\\\"complete\\\",c)})}function h(c){b.message+=c.message;b.info=\\\"FAILED\\\";b.status=0;a&&a(\\\"error\\\",b);g.event.O(d,\\\"error\\\",b)}d.kA?d.i4(c,h):h({message:\\\"GeoLocation is closed by noGeoLocation option,pass Geolocation.\\\"})} var d=this;d.Ew?d.n4(b,c):c({message:\\\"IpLocation is closed by noIpLocate option,pass IpLocation.\\\"})},i4:function(a,b){if(this.isSupported()){var c=this;g.a.HR(function(d,e){if(d)c.Ul(\\\"err\\\"),b({message:d?d.message:\\\"\\\"});else{var f=setTimeout(function(){f=null;b({message:\\\"Get geolocation time out.\\\"});c.Ul(\\\"waitetimeout\\\");h=!0},c.options.timeout),h=!1;e.getCurrentPosition(function(d){if(!h)if(clearTimeout(f),d.coords&&d.coords.longitude){c.Ul(\\\"success\\\");d={position:new g.U(d.coords.longitude,d.coords.latitude), location_type:\\\"html5\\\",message:\\\"Get geolocation success.\\\",accuracy:d.coords.accuracy};var e=!1;g.l.Q3&&(e=!0);c.options.convert?e?(d.isConverted=!0,d.message+=\\\"Don't need convert.\\\",a(d)):c.sI(d,function(b){a(b)}):(e?(d.message+=\\\"Don't need convert.\\\",d.isConverted=!0):(d.isConverted=!1,d.message+=\\\"convert option is false,pass convert.\\\"),a(d))}else c.Ul(\\\"failed\\\"),b({code:2,info:\\\"POSITION_UNAVAILABLE\\\",message:\\\"Get geolocation failed.\\\"})},function(a){h||(clearTimeout(f),1===a.code?(a={code:1,info:\\\"PERMISSION_DENIED\\\", message:\\\"Geolocation permission denied.\\\"},c.Ul(\\\"denied\\\"),b(a)):3===a.code?(a={code:3,info:\\\"TIME_OUT\\\",message:\\\"Get geolocation time out.\\\"},c.Ul(\\\"timeout\\\"),b(a)):2===a.code&&(a={code:3,info:\\\"POSITION_UNAVAILABLE\\\",message:\\\"Get geolocation failed.\\\"},c.Ul(\\\"failed\\\"),b(a)))},this.options)}},{timeout:c.options.timeout})}else b({message:\\\"Browser not Support html5 geolocation.\\\"})},sI:function(a,b){var c=a.position,c=this.options.convertUrl.replace(\\\"{x}\\\",c.R).replace(\\\"{y}\\\",c.Q);g.o.Xa&&(c+=\\\"&jscode=\\\"+g.o.Xa); c=new g.kb.Ab(c,{callback:\\\"callback\\\"});g.event.addListener(c,\\\"complete\\\",function(c){1===parseInt(c.status)&&c.locations?(c=c.locations.split(\\\",\\\"),c=new g.U(parseFloat(c[0]),parseFloat(c[1])),a.position=c,a.isConverted=!0,a.message+=\\\"Convert Success.\\\"):(a.isConverted=!1,a.message+=\\\"Convert failed.\\\");b(a)},this);g.event.addListener(c,\\\"error\\\",function(){a.message+=\\\"Convert request failed.\\\";a.isConverted=!1;b(a)},this)},n4:function(a,b){var c=new g.kb.Ab(g.o.Cc+\\\"://webapi.amap.com/maps/ipLocation?key=\\\"+ g.o.key,{callback:\\\"callback\\\"});c.h(\\\"complete\\\",function(c){c.status&&c.lng&&c.lat?(c.position=g.a.Moa(c.lng,c.lat),delete c.lng,delete c.lat,c.message=\\\"Get ipLocation success.\\\",c.location_type=\\\"ip\\\",c.accuracy=null,c.isConverted=!0,a(c)):b({message:\\\"Get ipLocation failed.\\\"})},this);c.h(\\\"error\\\",function(){b({message:\\\"Request ipLocation failed.\\\"})},this)},Gqa:function(a){function b(b){function e(e){e.message=b.message+e.message;e.status=1;c.Lv(e,function(b,d){d.info=\\\"SUCCESS\\\";a&&a(\\\"complete\\\",d);g.event.O(c, \\\"complete\\\",d)})}function f(e){b.message+=e.message;if(1===e.code)b.info=\\\"FAILED\\\",b.status=0,a&&a(\\\"error\\\",b),g.event.O(c,\\\"error\\\",b);else{e=function(e){b.message+=e.message;b.info=\\\"FAILED\\\";b.status=0;a&&a(\\\"error\\\",b);g.event.O(c,\\\"error\\\",b)};var f=function(e){e.status=1;e.message=b.message+e.message;c.Lv(e,function(b,d){d.info=\\\"SUCCESS\\\";a&&a(\\\"complete\\\",d);g.event.O(c,\\\"complete\\\",d)})};c.Ew?c.n4(f,e):e({message:\\\"IpLocation is closed by noIpLocate option,pass ipLocation.\\\"})}}c.kA?c.i4(e,f):f({message:\\\"Geolocation is closed by noGeoLocation option,pass Geolocation.\\\"})} var c=this;this.options.useNative&&g.l.Zl?this.Uta(function(b){b.status=1;c.Lv(b,function(b,d){d.info=\\\"SUCCESS\\\";a&&a(\\\"complete\\\",d);g.event.O(c,\\\"complete\\\",d)})},function(a){b(a)}):b({message:\\\"\\\"})},getCurrentPosition:function(a){g.c.add(this,\\\"getCurrentPosition\\\");this.jO=!0;g.f.Wa(this.hi,\\\"amap-locate-loading\\\");g.l.ba||this.options.GeoLocationFirst?this.Gqa(a):this.Hqa(a)},getCityInfo:function(a){g.c.add(this,\\\"getCityInfo\\\");var b=new g.kb.Ab(g.o.Cc+\\\"://webapi.amap.com/maps/ipCity?key=\\\"+g.o.key,{callback:\\\"callback\\\"}); b.h(\\\"complete\\\",function(b){b.center&&1===b.status?(b.isConverted=!0,b.info=\\\"SUCCESS\\\",b.message=\\\"Get city by ip success.\\\",a&&a(\\\"complete\\\",b)):(b.info=\\\"FAILED\\\",b.message=\\\"Get city by ip failed.\\\",a&&a(\\\"error\\\",b))},this);b.h(\\\"error\\\",function(){a&&a(\\\"error\\\",{status:0,message:\\\"Request ipCity failed.\\\",info:\\\"FAILED\\\"})},this)},watchPosition:function(){if(this.options.useNative&&window.AMapAndroidLoc)this.dAa();else if(g.c.add(this,\\\"watchPosition\\\"),this.isSupported()){if(g.l.ba){var a=this;return g.a.HR(function(b, c,d){if(b)g.event.O(a,\\\"error\\\",{status:0,info:\\\"FAILED\\\",message:\\\"Get geolocation error on watch.\\\"});else return b=c.watchPosition(function(b){if(b.coords&&b.coords.longitude){b={position:new g.U(b.coords.longitude,b.coords.latitude),location_type:\\\"html5\\\",message:\\\"Watch position success by GeoLocation.\\\",status:1,accuracy:b.coords.accuracy};var c=!1;g.l.Q3&&(c=!0);var d=function(b){a.Lv(b,function(b,c){c.info=\\\"SUCCESS\\\";g.event.O(a,\\\"complete\\\",c)})};a.options.convert?c?(b.message+=\\\"Don't need convert.\\\", b.isConverted=!0,d(b)):a.sI(b,function(a){d(a)}):(c?(b.message+=\\\"Don't need convert.\\\",b.isConverted=!0):(b.message+=\\\"convert option is false,pass convert.\\\",b.isConverted=!1),d(b))}else g.event.O(a,\\\"error\\\",{status:0,info:\\\"FAILED\\\",message:\\\"Watch position failed by GeoLocation,incorrect position.\\\"})},function(){g.event.O(a,\\\"error\\\",{status:0,info:\\\"FAILED\\\",message:\\\"Watch position failed by GeoLocation.\\\"})},a.options),d&&g.a.Mxa(d,b),b},{aAa:!0})}g.event.O(this,\\\"error\\\",{status:0,info:\\\"NOT_SUPPORT\\\",message:\\\"Forbidden watchPosition on PC device.\\\"})}else return g.event.O(this, \\\"error\\\",{status:0,info:\\\"NOT_SUPPORT\\\",message:\\\"Browser not Support html5 GeoLocation.\\\"}),null},clearWatch:function(a){g.c.add(this,\\\"clearWatch\\\");if(this.options.useNative&&window.AMapAndroidLoc)g.Vn.Mw.Bi.mya(this);else if(this.isSupported())return g.a.HR(function(b,c){b||g.a.$za(a,function(b,e){b||(g.a.Jna(a),c.clearWatch(e))})})},zu:function(a,b){if(!this.hi){var c=document.createElement(\\\"div\\\");this.jO&&g.f.Wa(c,\\\"amap-locate-loading\\\");g.f.Wa(c,\\\"amap-geolocation-con\\\");\\\"object\\\"===typeof this.options.buttonDom? c.appendChild(this.options.buttonDom):c.innerHTML=this.options.buttonDom;c.style.position=\\\"absolute\\\";c.style.zIndex=\\\"9999\\\";this.options.showButton||(c.style.visibility=\\\"hidden\\\");this.cC(c,this.options.buttonPosition,this.options.buttonOffset);this.hi=c}this.e=a;b.appendChild(this.hi);this.rx=b;this.MM=g.event.Y(this.hi,\\\"click\\\",this.Nua,this)},Yv:function(){g.event.removeListener(this.bY);this.bY=null;g.event.removeListener(this.uY);this.uY=null;g.event.removeListener(this.MM);this.MM=null;this.rx.removeChild(this.hi); this.e=this.rx=null;this.Tb&&(this.Tb.setMap(null),this.Tb=null);this.Lb&&(this.Lb.setMap(null),this.Lb=null)},cC:function(a,b,c){\\\"LT\\\"!==b&&\\\"RT\\\"!==b&&\\\"LB\\\"!==b&&\\\"RB\\\"!==b&&(b=\\\"LT\\\");switch(b){case \\\"LT\\\":g.f.ab(a,{left:c.x+\\\"px\\\",top:c.y+\\\"px\\\",right:\\\"\\\",bottom:\\\"\\\"});break;case \\\"RT\\\":g.f.ab(a,{right:c.x+\\\"px\\\",top:c.y+\\\"px\\\",left:\\\"\\\",bottom:\\\"\\\"});break;case \\\"LB\\\":g.f.ab(a,{left:c.x+\\\"px\\\",bottom:c.y+\\\"px\\\",right:\\\"\\\",top:\\\"\\\"});break;case \\\"RB\\\":g.f.ab(a,{right:c.x+\\\"px\\\",bottom:c.y+\\\"px\\\",left:\\\"\\\",top:\\\"\\\"})}this.rx&&(this.rx.removeChild(a), this.rx.appendChild(a))},$xa:function(a,b){if(this.e){var c=this.options;c.showMarker&&(this.Tb||(this.Tb=new z.B.wb(c.markerOptions),this.Tb.Da=!0),this.Tb.setPosition(a),this.Tb.setMap(this.e));c.showCircle&&(this.Lb||(this.Lb=new z.B.hh(c.circleOptions)),this.Lb.setCenter(a),this.Lb.setRadius(b||0),this.Lb.setMap(this.e),this.Lb.Da=!0);c.panToLocation&&this.e.panTo(a);c.zoomToAccuracy&&this.Lb&&this.e.setFitView([this.Lb])}},cFa:function(){this.Tb&&this.Tb.setMap(null);this.Lb&&this.Lb.setMap(null)}, Lv:function(a,b){var c=this,d=a.position;g.ub.load(\\\"AMap.Geocoder\\\",function(){c.R3||(c.R3=new g.lW({extensions:c.options.extensions}));c.R3.getAddress(d,function(c,d){\\\"complete\\\"===c?(g.extend(a,d.regeocode),a.message+=\\\"Get address success.\\\"):a.message+=\\\"Get address fail,check your key or network.\\\";b(\\\"complete\\\",a)})})},Nua:function(){this.getCurrentPosition()},Oua:function(a){this.jO=!1;g.f.fb(this.hi,\\\"amap-locate-loading\\\");this.$xa(a.position,a.accuracy)},Pua:function(){this.jO=!1;g.f.fb(this.hi, \\\"amap-locate-loading\\\")},Uta:function(a,b){function c(a){1!==a&&d.Ul(\\\"sdkerror\\\");b&&b.call&&b.call(d,{message:\\\"Get sdkLocation failed.\\\"})}if(window.AMapAndroidLoc)this.Vta(a,b);else{var d=this,e=new g.kb.Ab(\\\"http://127.0.0.1:43689/geolocation?to=3000&_=\\\"+(new Date).getTime(),{callback:\\\"callback\\\"});e.h(\\\"complete\\\",function(b){a&&a.call&&b.location&&b.location.x&&b.location.y?(a.call(d,{position:new g.U(b.location.x,b.location.y),accuracy:b.location.precision,message:\\\"Get sdkLocation success.\\\",location_type:\\\"sdk\\\", isConverted:!0}),d.Ul(\\\"sdksuccess\\\")):(c(1),d.Ul(\\\"sdkfail\\\"))});e.h(\\\"error\\\",c)}},Vta:function(a,b){var c=this;g.Vn.Mw.Bi.gra(this,function(d){a&&a.call&&d.result&&d.result.x&&d.result.y?(a.call(c,{position:new g.U(d.result.x,d.result.y),accuracy:d.result.precision,message:\\\"Get sdkLocation success.\\\",location_type:\\\"sdk\\\",isConverted:!0}),c.Ul(\\\"sdksuccess\\\")):(b&&b.call&&b.call(c,{sdkErrorInfo:d.errorInfo,sdkLocationDetail:d.locationDetail,message:\\\"Get sdkLocation failed.\\\"}),c.Ul(\\\"sdkfail\\\"))})},dAa:function(){var a= this;g.Vn.Mw.Bi.cAa(this,function(b){b.result&&b.result.x&&b.result.y?(b={position:new g.U(b.result.x,b.result.y),location_type:\\\"sdk\\\",message:\\\"Watch position success by Android SDK.\\\",status:1,accuracy:b.result.precision},b.message+=\\\"Don't need convert.\\\",b.isConverted=!0,function(b){a.Lv(b,function(b,c){c.info=\\\"SUCCESS\\\";g.event.O(a,\\\"complete\\\",c)})}(b)):g.event.O(a,\\\"error\\\",{status:0,info:\\\"FAILED\\\",message:\\\"Watch position failed by Android SDK,incorrect position.\\\"})})}});window.AMap.Geolocation=g.Vn; g.Vn.cbk=function(a){a=JSON.parse(a);var b=g.Vn.Mw.Bi;b.r(\\\"get\\\",a);b.r(\\\"watch\\\",a)}; g.Vn.Mw=g.da.extend({ka:[g.va],A:function(){},gra:function(a,b){var c=window.AMapAndroidLoc;this.h(\\\"get\\\",b,a,!0);this.we(\\\"watch\\\")||c.getLocation(JSON.stringify({to:a.options.timeout,useGPS:1,watch:0,callback:\\\"AMap.Geolocation.cbk\\\"}))},cAa:function(a,b){this.we(\\\"watch\\\")||window.AMapAndroidLoc.getLocation(JSON.stringify({to:a.options.timeout,useGPS:1,watch:1,interval:a.options.watchInterval||5,callback:\\\"AMap.Geolocation.cbk\\\"}));this.h(\\\"watch\\\",b,a)},mya:function(a){var b=window.AMapAndroidLoc;this.fK(\\\"watch\\\", a);this.we(\\\"watch\\\")||b.stopLocation()}});g.Vn.Mw.Bi=new g.Vn.Mw; \"}"}, {"name": "_AMap_mouse", "value": "{\"version\":\"1768801478717\",\"script\":\"g.Sb.Ib({rN:\\\"_docMsMov\\\",Kca:function(){var a=this.Ya.ad;g.F.h(a,\\\"mousedown\\\",this.S4,this);g.F.h(document,\\\"mousedown\\\",this.Y4,this);this.Jpa||g.l.Ve||(this.hw=g.a.ND(this.hw,150,this),this.Jpa=!0);this.v7();g.F.h(document,\\\"mouseout\\\",this.hw,this);this.h(this.rN,this.N6,this);g.F.h(a,\\\"mouseover\\\",this.T4,this);g.F.h(a,\\\"mouseup\\\",this.U4,this);g.F.h(a,\\\"contextmenu\\\",this.KF,this);g.F.h(a,\\\"rightclick\\\",this.KF,this)},Eha:function(){var a=this.Ya.ad;g.F.G(a,\\\"mousedown\\\",this.S4,this);g.F.G(document,\\\"mousedown\\\", this.Y4,this);this.Ona();g.F.G(document,\\\"mouseout\\\",this.hw,this);this.G(this.rN,this.N6,this);g.F.G(a,\\\"mouseover\\\",this.T4,this);g.F.G(a,\\\"mouseup\\\",this.U4,this);g.F.G(a,\\\"contextmenu\\\",this.KF,this);g.F.G(a,\\\"rightclick\\\",this.KF,this)},Mca:function(){g.F.h(this.Ya.ad,\\\"mousewheel\\\",this.R4,this)},r_:function(){g.F.G(this.Ya.ad,\\\"mousewheel\\\",this.R4,this)},Pna:function(){g.F.G(document,\\\"mousemove\\\",this.hw,this);g.F.G(this.Ya.ad,\\\"mousemove\\\",this.$R,this)},v7:function(){g.F.h(document,\\\"mousemove\\\",this.hw,this); g.F.h(this.Ya.ad,\\\"mousemove\\\",this.$R,this)},Ona:function(){g.F.G(document,\\\"mousemove\\\",this.hw,this);g.F.G(this.Ya.ad,\\\"mousemove\\\",this.$R,this)},T4:function(){this.IG=!0},Y4:function(){this.IG=!1},hw:function(a){if(!this.Kf&&!this.sg&&this.Ya){var b=this.Mi(a,!0,!0);this.r(this.rN,{event:a,TJ:b})}},Lca:function(){this.h(\\\"mousemove\\\",this.P_);this.h(\\\"mouseout\\\",this.Q_);this.h(\\\"mouseover\\\",this.R_);this.h(\\\"mouseup\\\",this.S_);this.h(\\\"mousedown\\\",this.O_);this.h(\\\"rightclick\\\",this.U_);this.h(\\\"contextmenu\\\", this.G_);this.h(\\\"rdblclick\\\",this.V_)},Gha:function(){this.G(\\\"mousemove\\\",this.P_);this.G(\\\"mouseout\\\",this.Q_);this.G(\\\"mouseover\\\",this.R_);this.G(\\\"mouseup\\\",this.S_);this.G(\\\"mousedown\\\",this.O_);this.G(\\\"rightclick\\\",this.U_);this.G(\\\"contextmenu\\\",this.G_);this.G(\\\"rdblclick\\\",this.V_)},keyboardEnableChanged:function(){this.get(\\\"keyboardEnable\\\")?this.Ica():this.CO()},scrollWheelChanged:function(){this.get(\\\"scrollWheel\\\")?this.h(\\\"mousewheel\\\",this.T_):this.G(\\\"mousewheel\\\",this.T_)},KF:function(a){g.F.preventDefault(a)}, vha:function(a){var b=this.Mi(a,!1,!0),c=0;\\\"3D\\\"==this.D.view.type?(this.qa(\\\"mousewheel\\\",b),a.wheelDelta?(c=a.wheelDelta,window.opera&&g.a.Qh(window.opera.version)&&9.5>parseFloat(window.opera.version())&&(c=-c),0==c/120%1&&(c/=2)):a.detail&&(c=-a.detail,120!==c&&-120!==c&&(c*=20)),a=c/800,a=this.get(\\\"zoom\\\")+a,this.x2(a,b.Ta,!0)):(a.wheelDelta?(c=a.wheelDelta/120,window.opera&&g.a.Qh(window.opera.version)&&9.5>parseFloat(window.opera.version())&&(c=-c)):a.detail&&(c=-a.detail),this.V.VC=(this.V.VC|| 0)+c,this.r(\\\"mousewheel\\\",b))},R4:function(a){\\\"info\\\"!=a.af&&this.get(\\\"scrollWheel\\\")&&this.vha(a)},cS:function(a,b){b=this.Mi(a,!1,!0);if(!this.Sv(b.Db,this.V.Hx)){var c=this.V;c.Hx=b.Db;if(c.Cr){if(!c.Cd&&(b.Db.x!==c.jo.x||b.Db.y!==c.jo.y)){var d;this.$N(c.au)&&c.au.get(\\\"draggable\\\")&&(d=c.au);!d&&this.get(\\\"dragEnable\\\")&&(d=this);d&&(c.Cd=!0,c.kq=d,c.gg=c.jo,c.jq=c.NT)}c.Cd&&(c.bk=b,b.button=a.buttons||a.button||0,this.UO())}}},qJ:function(){var a=this.TJ;g.f.fb(this.K,\\\"amap-drag\\\");g.f.Gpa();g.f.t3(); this.V&&(this.V.Cd=!1);this.V.Gp&&(this.V.Gp=!1,this.V.kq.r(\\\"dragend\\\",a));this.V.Cr=!1;this.V.Hx=null;this.V.gg=null;g.F.G(document,\\\"mousemove\\\",this.cS,this);g.F.G(document,\\\"mouseup\\\",this.qJ,this);try{g.F.G(window.parent.document,\\\"mouseup\\\",this.qJ,this)}catch(b){}this.Ya&&(this.Kf&&(this.sg=this.Kf=!1),g.F.Cza(this.Ya.ad),this.v7())},Rra:function(a,b){b=b||this.Mi(a,!1,!1,a.srcElement||a.target);var c=this.get(\\\"size\\\"),d;0<=b.Db.x&&b.Db.x<=c.width&&0<=b.Db.y&&b.Db.y<=c.height&&(d=!0);this.L5&&!d?this.r(\\\"mouseout\\\", b):!this.L5&&d&&this.r(\\\"mouseover\\\",b);this.L5=d},$R:function(a){if(!this.Kf&&!this.sg&&this.Ya&&\\\"info\\\"!=a.af&&(g.F.preventDefault(a),g.l.Gs&&(a=window.event),a)){a=this.Mi(a,!1,!1,a.srcElement||a.target);var b=this.V,c=a.kd,d;if(c){this.V.Cr||(c instanceof g.B.Fh?d=c.Mb.get(\\\"cursor\\\"):c instanceof g.q.Zb&&(d=c.get(\\\"cursor\\\")),d&&c!==this||(d=this.D.De.defaultCursor),d=this.D.get(\\\"optimalCursor\\\")||d,this.K.style.cursor=d||\\\"\\\");d=b.Wga;var e=a.uu;d!==e&&(d&&b.ZZ&&b.ZZ.r(\\\"mouseout\\\",a),e&&c&&(c.r(\\\"mouseover\\\", a),c.get(\\\"topWhenMouseOver\\\")&&(c.Mb.C=!0,c.Mb.setTop(!0),c.Mb.C=!1)));a.kd.r(\\\"mousemove\\\",a);a.kd!==this&&this.r(\\\"mousemove\\\",a);b.Wga=a.uu||null;b.ZZ=a.kd||null}}},S4:function(a){if(\\\"info\\\"!=a.af&&(this.IG=!0,g.f.Xoa(),g.f.g3(),this.V.Hx=null,this.V.gg=null,\\\"info\\\"!=a.af)){var b=this.Mi(a,!0,!1,a.srcElement||a.target);if(b.kd){this.V.k3=new Date;this.V.au=b.kd;this.V.jo=b.Db;this.V.Sga=b.button;this.V.Cr=!0;this.V.Tga=b;this.V.Cd||(this.V.NT=b);this.Ie&&this.Ie.stop();var c=b.kd;!c.Mb&&c.get(\\\"dragEnable\\\")&& (this.V.Cr=!0);g.f.Wa(this.K,\\\"amap-drag\\\");c=!1;b.kd.r(\\\"mousedown\\\",b);b.kd!==this&&(this.r(\\\"mousedown\\\",b),c=b.kd.get(\\\"draggable\\\"));c=c||this.get(\\\"dragEnable\\\");g.F.Bxa(this.Ya.ad);g.F.h(document,\\\"mousemove\\\",this.cS,this);g.F.h(document,\\\"mouseup\\\",this.qJ,this);try{window.parent.document&&g.l.Gs&&g.F.h(window.parent.document,\\\"mouseup\\\",this.qJ,this)}catch(d){}g.F.stopPropagation(a);c&&this.Pna()}}},N6:function(a){this.Rra(a.event,a.TJ)},JDa:function(a){this.cS(a.event,a.TJ)},U4:function(a){if(\\\"info\\\"!= a.af){this.TJ=a=this.Mi(a,!1,!1,a.srcElement||a.target);var b=a.kd;if(b){var c=this.D.get(\\\"allBubble\\\")||b.get(\\\"bubble\\\");b!==document&&(b.r(\\\"mouseup\\\",a),a.kd!==this&&c&&this.r(\\\"mouseup\\\",a));this.V.jo&&this.Sv(a.Db,this.V.jo)&&(this.bg&&this.Sv(this.V.Ex,a.Db)&&a.button===this.V.Sga?(clearTimeout(this.bg),this.bg=null,g.F.R5(a)?c?(b.r(\\\"dblclick\\\",a),b!==this&&this.r(\\\"dblclick\\\",a)):b.r(\\\"dblclick\\\",a):b.r(\\\"rdblclick\\\",a),this.V.Ex=null):(g.F.R5(a)?(this.r(\\\"clickstart\\\",a),c?(b.r(\\\"click\\\",a),b!==this&&this.r(\\\"click\\\", a)):(b.r(\\\"click\\\",a),this.r(\\\"closeOverlays\\\",a)),b.get(\\\"topWhenClick\\\")&&(b.Mb.C=!0,b.Mb.setTop(!0),b.Mb.C=!1),this.r(\\\"clickend\\\",a)):c?(b.r(\\\"rightclick\\\",a),b!==this&&this.r(\\\"rightclick\\\",a),this.r(\\\"contextmenu\\\",a)):(b.r(\\\"rightclick\\\",a),b.r(\\\"contextmenu\\\",a)),this.V.Ex=a.Db,this.bg&&clearTimeout(this.bg),this.bg=setTimeout(g.a.bind(function(){clearTimeout(this.bg);this.bg=null;this.V.Ex=null},this),260)))}}}});g.Sb.Ib({T_:function(a){this.qa(\\\"mousewheel\\\",a);var b=this;if(1<=Math.abs(this.V.VC)&&!this.T$){var c=this.V.VC;if(1<=c)c=1;else if(-1>=c)c=-1;else return;b.eH(a.Ta,c);this.V.VC=0;this.T$=!0;setTimeout(function(){b.T$=!1;b.V.VC=0},g.l.pz?50:40)}},P_:function(a){this.qa(\\\"mousemove\\\",a)},Q_:function(a){this.qa(\\\"mouseout\\\",a)},R_:function(a){this.qa(\\\"mouseover\\\",a)},S_:function(a){this.qa(\\\"mouseup\\\",a)},O_:function(a){this.qa(\\\"mousedown\\\",a)},U_:function(a){this.qa(\\\"rightclick\\\",a)},G_:function(a){this.qa(\\\"contextmenu\\\", a)},V_:function(a){this.get(\\\"doubleClickZoom\\\")&&this.get(\\\"zoomEnable\\\")&&this.eH(a.Ta,-1);this.qa(\\\"rdblclick\\\",a)}});g.Sb.Ib({yga:function(){this.BB={left:[37],right:[39],IQ:[40],dF:[38],Qi:[187,107,61],Ri:[189,109,173]};this.uda={left:this.by(this.cH(100,0)),right:this.by(this.cH(-100,0)),IQ:this.by(this.cH(0,-100)),dF:this.by(this.cH(0,100)),Qi:this.by(this.L1(1)),Ri:this.by(this.L1(-1))}},Ica:function(){this.get(\\\"keyboardEnable\\\")&&(this.BB||this.yga(),g.F.h(document,\\\"keydown\\\",this.aH,this),g.F.h(document,\\\"keyup\\\",this.C_,this))},CO:function(){g.F.G(document,\\\"keydown\\\",this.aH,this);g.F.G(document,\\\"keyup\\\",this.C_, this)},C_:function(a){var b=a.keyCode;!a.ctrlKey||37!==b&&39!==b||this.set(\\\"refresh\\\",1)},aH:function(a){var b=a.keyCode,c;for(c in this.BB)if(this.BB.hasOwnProperty(c))for(var d=0;de&&a.bottom>c?a.top+d-c-2+\\\"px\\\":a.bottom+d+2+\\\"px\\\";this.Vb.style.minWidth=b+2+\\\"px\\\";return this},Er:function(a,b,c){for(var d in a)\\\"undefined\\\"!==typeof a[d]&&\\\"undefined\\\"!==typeof b[d]&&c.push(b[d]+\\\"=\\\"+a[d])}});window.AMap.Autocomplete=g.VV; \"}"}, {"name": "_AMap_AMap.Driving", "value": "{\"version\":\"1768801478717\",\"script\":\"g.Ti=g.Ti?g.Ti:{LEAST_TIME:0,LEAST_FEE:1,LEAST_DISTANCE:2,REAL_TRAFFIC:4};g.RHa={\\\"\\\\u901f\\\\u5ea6\\\\u6700\\\\u5feb\\\":0,\\\"\\\\u8d39\\\\u7528\\\\u6700\\\\u4f4e\\\":1,\\\"\\\\u8ddd\\\\u79bb\\\\u6700\\\\u77ed\\\":2,\\\"\\\\u4e0d\\\\u8d70\\\\u5feb\\\\u901f\\\\u8def\\\":3,\\\"\\\\u53c2\\\\u8003\\\\u4ea4\\\\u901a\\\\u4fe1\\\\u606f\\\\u6700\\\\u5feb\\\":4}; g.Pw=g.da.extend({ka:[g.va],A:function(a){this.CLASS_NAME=\\\"AMap.Driving\\\";g.a.zb(this);g.c.ya(this,a);this.N=g.extend({showTraffic:!0,showDir:!0,policy:0,outlineColor:\\\"#ffe\\\",isOutline:!0,autoFitView:!0},a);this.url=g.o.fd+\\\"/v3/direction/driving\\\";this.Qg=[];(this.N.map||this.N.panel)&&g.ub.load(\\\"AMap.DrivingRender\\\")},oZ:function(){return Math.round(1E5*Math.random())},clear:function(){g.c.add(this,\\\"clear\\\");this.M&&this.M.clear()},search:function(){g.c.add(this,\\\"search\\\");var a=arguments;if(0=f.width||b>=f.height)window.clearInterval(e),g.f.ab(f.j.K,{width:f.width+\\\"px\\\",height:f.height+\\\"px\\\"}).ab(f.j.button, {backgroundPosition:\\\"-40px -386px\\\"}),f.bo&&g.f.ab(f.bo,{right:f.width+\\\"px\\\"}),f.En=!0,f.j.K.removeChild(f.j.pk),f.j.K.insertBefore(f.j.pk,f.j.button)},15);g.event.O(this,\\\"open\\\")},close:function(){g.c.add(this,\\\"close\\\");var a=this.width,b=this.width,c=Math.round((this.width-17)/10),d=Math.round((this.height-17)/10),e,f=this;e=setInterval(function(){a-=c;b-=d;g.f.ab(f.j.K,{width:a+\\\"px\\\",height:b+\\\"px\\\"});f.bo&&g.f.ab(f.bo,{right:a+\\\"px\\\"});if(17>=a||17>=b)window.clearInterval(e),g.f.ab(f.j.K,{width:\\\"17px\\\", height:\\\"17px\\\"}).ab(f.j.button,{backgroundPosition:\\\"-40px -405px\\\"}),f.bo&&g.f.ab(f.bo,{right:\\\"17px\\\"}),f.En=!1,f.j.K.removeChild(f.j.pk),f.j.K.insertBefore(f.j.pk,f.j.button)},15);g.event.O(this,\\\"close\\\")},getTileLayer:function(){g.c.add(this,\\\"getTileLayer\\\");return this.ow},setTileLayer:function(a){g.c.add(this,\\\"setTileLayer\\\");this.ow=a;this.map&&this.jC()},show:function(){g.c.add(this,\\\"show\\\");this.vb({visible:!0});g.event.O(this,\\\"show\\\")},hide:function(){g.c.add(this,\\\"hide\\\");this.vb({visible:!1});g.event.O(this, \\\"hide\\\")},jC:function(){var a=this.map.getSize(),b=this.map.getCenter(),c=this.Za,d=this.ufa(a.width,a.height,this.width-5,this.height-5);a.width*a.height*Math.pow(2,2*d)*4>this.width*this.height&&(d-=1);var e=this.map.getZoom()+d;this.f3=d;var f=0,h=0;e>=c[0]&&e<=c[1]?(f=Math.round(Math.pow(2,d)*a.width),h=Math.round(Math.pow(2,d)*a.height)):e=c[0];this.Vka(f,h);this.Lka(b,e)},Lka:function(a,b){this.Yk||(this.Yk=[]);for(var c=this.map.lc(a,b),d=this.width-5,e=this.height-5,f=Math.floor((c.y-e/2)/ 256),h=Math.floor((c.x+d/2)/256),k=Math.floor((c.y+e/2)/256),l=0,m=Math.floor((c.x-d/2)/256);m<=h;m+=1)for(var n=f;n<=k;n+=1){this.Yk.length<=l&&this.Yk.push(g.f.create(\\\"img\\\",this.j.map));var p=this.Yk[l];g.f.ab(p,{visibility:\\\"inherit\\\",position:\\\"absolute\\\",left:256*m-c.x+d/2+\\\"px\\\",top:256*n-c.y+e/2+\\\"px\\\"});p.src=this.Ar(m,n,b);l+=1}for(;ls[1]?Math.PI:0:x[1]===s[1]?s=x[0]>s[0]?Math.PI/2:-Math.PI/2:(m=x[0]-s[0],x=x[1]-s[1],s=Math.atan(m/x),0m&&0>x?s=-s:0x?s=-s:0>m&&0b[2]?1:a.v5>b.v5?1:-1});for(var u={},v=0,t=q.length;vb[2]?1:a[2]r[1].length)break a;l=r[1];h=r[0]}else 2l;l++)h.push(c[0]+d*Math.cos(e),c[1]+d*Math.sin(e)),e+=f,0Math.sqrt(3)/2&&(d=Math.sqrt(3)/2);c=c.ps(d)}return c}function f(a,c,e){if(void 0===e||!0===e)b.push(a[0],a[1]),b.push(-c.y,c.x,0,0),b.push(n,0,0),d.push(a[2]);if(void 0===e||!1===e)b.push(a[0],a[1]),b.push(c.y,-c.x,0,0),b.push(n,0,0),d.push(a[2])}for(var k=a[0],l=a[a.length-1],n=0,r=void 0;k[0]===l[0]&&k[1]===l[1]&&k!==l;)a.pop(), l=a[a.length-1];a.push(k);a.push(a[1]);a.unshift(l);var q=a.length;a.forEach(function(a,d,k){var l=k[d-1];k=k[d+1];var u=void 0;l&&(n+=s.Vd(a,l));if(0===d||d===q-1)0===d?(u=s.normalize(new h(k[0]-a[0],k[1]-a[1])),c.push(0,1,2)):(u=s.normalize(new h(a[0]-l[0],a[1]-l[1])),d=b.length/p+2,c.push(d-3,d-1,d-2)),f(a,u);else{d=s.normalize(new h(a[0]-l[0],a[1]-l[1]));k=s.normalize(new h(k[0]-a[0],k[1]-a[1]));l=s.normalize(d.multiply(-1).sk(k));r=s.FS(l,d);f(a,e(d,k),!0);f(a,e(d,k),!1);f(a,e(d,k),!0);f(a,e(d, k),!1);a=b.length/p-4;k=a+2;var u=a*p,t=u+p,E=t+p,C=E+p;d=Math.abs(s.cos(d,l)/s.sin(d,l));l=m;r?(b[u+l]=d,b[t+l]=-d,b[E+l]=-d,b[C+l]=d):(b[u+l]=-d,b[t+l]=d,b[E+l]=d,b[C+l]=-d);c.push(a,a-1,a+1);c.push(k,k+1,k+2)}})}function e(c,d,e){function f(a,b){if(r)for(;0=a&&(a+=2*Math.PI);return a}};h.prototype={sk:function(a){return new h(this.x+a.x,this.y+a.y)},zf:function(a){return new h(this.x-a.x,this.y-a.y)},multiply:function(a){return\\\"number\\\"===typeof a?new h(this.x*a,this.y*a):this.x*a.x+this.y*a.y}, ps:function(a){return new h(this.x/a,this.y/a)},$u:function(a){var b=a.y;return this.x===a.x&&this.y===b}};return{parse:function(a,b){var h=a.lineJoin,h=void 0===h?\\\"bevel\\\":h,k=a.lineCap,k=void 0===k?\\\"butt\\\":k,l=a.Vsa,l=void 0===l?!1:l,m=a.ez,n=void 0===m?!1:m,r=a.y7,m=a.Tya,s=void 0===m?!0:m,m=a.Lsa,q=[],B=[],G=f(a.Rv,void 0===r?!0:r),r=[];l?(d(G,q,B,r),h=q.length,[0,1,2,h/p-1,h/p-2].forEach(function(a){q[a*p+8]=-1})):(c(G,q,B,h,n,r),e(G,q,B,k,r));s&&(q=new Float32Array(q),B=655351) {v_op=a_op.xy;v_TB=a_op.z;vec3 curPos=position.xyz;v_distance=a_Next.z;float ddis=0.0;vec3 previous=vec3(a_Previous.x+u_xDelta,a_Previous.y,0);vec3 next=vec3(a_Next.x+u_xDelta,a_Next.y,0);vec4 up,dir;v_distance *= u_scale;if(previous==curPos){ dir= vec4(normalize(next-curPos).xy,0,0);}else if(next==curPos){dir= -vec4(normalize(previous-curPos).xy,0,0);}else {vec3 dir0=previous-curPos;vec3 dir1=next-curPos;vec3 dir2=normalize(dir0);vec3 dir3=normalize(dir1);float f0=dir0.x*dir1.y-dir1.x*dir0.y;dir = vec4(0); v_cap=1.0;if(f0==0.0){up = vec4(dir3.y,-dir3.x,0,0); }else{vec3 dir4=normalize(dir2+dir3);float sinA=length(cross(dir4,dir2));float cosA=dot(dir4,dir2);if(sinA<0.5){sinA=0.5;}up= vec4(dir4,0)/sinA;ddis=abs(length(up)*cosA*u_width*0.5);if(f0<0.0){up=-up;ddis=-ddis;}v_distance += v_TB*a_Previous.z*ddis;}}if(previous==curPos||next==curPos){ up = vec4(dir.y,-dir.x,0,0); if(u_type==4){v_cap=sqrt(v_TB*v_TB+a_Previous.z*a_Previous.z);}else if(u_type==3){v_cap=1.0;}else{dir = vec4(0);}}vec4 pos=u_zoomMatrix*u_modelMatrix*position;pos=pos+(up*v_TB-dir*a_Previous.z)*u_width*0.5;gl_Position=u_othMatrix*pos;}else if (u_type==1) {v_Tags=a_Tags;gl_Position=u_othMatrix*u_zoomMatrix*u_modelMatrix*position;}}\\\", raa:\\\"precision lowp float;uniform vec4 u_FragColor,u_FragColor2;varying lowp float v_distance,v_TB;varying vec2 v_op;varying lowp float v_Tags;varying float v_cap;uniform lowp int u_type;uniform int u_isDash,u_onlyBorder;uniform vec4 u_dash;void main() {vec4 color=u_FragColor;if (u_type==0) {}else if(u_type>1) {if(v_op.x<-0.2||v_op.x>256.2||v_op.y<-0.2||v_op.y>256.2){discard;}if(v_cap>1.0){discard;}if(u_isDash==1){float all = u_dash[0] + u_dash[1] + u_dash[2] + u_dash[3];float off = mod(v_distance - u_dash[0], all);if(off> u_dash[0]&&off<=(u_dash[0]+u_dash[1])||off>(all-u_dash[3])){discard;}}if(u_onlyBorder==1&&v_TB<=0.7&&v_TB>=-0.7){discard;}}else if(u_type==1) {color=v_Tags==3.0?u_FragColor2:vec4((u_FragColor+(u_FragColor2-u_FragColor)*v_Tags/3.0).xyzw);}gl_FragColor=color;}\\\", reloadChanged:function(){this.g&&(this.g.Ra=!1);this.oa.clear();this.xa&&this.xa.parentNode&&this.xa.parentNode.removeChild(this.xa);this.set(\\\"display\\\")},Bz:function(a){if(this.g&&a.PS===this.g.mc)for(var b=0,c=a.Pc.length;bthis.zoom))for(d.uniform1i(this.Ak,1),this.Zp(0,void 0,void 0),d.disableVertexAttribArray(this.hj),d.disableVertexAttribArray(this.gj),d.disableVertexAttribArray(this.Rg),d.enableVertexAttribArray(this.cy),f=0,h=e.length;fthis.N.pageSize?10:50a?10:50f&&(this.fq=f);this.hya()}},K7:function(){var a=new Date,b=!1;do if(b=this.parse(),new Date-a>=this.iW)break;while(!b);this.C7=b?null:g.a.Xc(this.K7,this)},hya:function(){this.C7|| (this.C7=g.a.Xc(this.K7,this))},woa:function(){return document.createElement(\\\"canvas\\\")},parse:function(){var a,b=!1,c=this.fq,d=this.pw[this.fq];if(d.vh())6===c&&(b=!0),this.fq=(this.fq+1)%7;else{a=d.Eua();var e=a[3],f=e.ja?512:256;if(\\\"groupcomplete\\\"===a[0])e.set(\\\"display\\\",0);else{var h=a[2];if(e.oa.Nd(h.key))if(\\\"co\\\"===a[0])if(3===a[1])this.OT(e,h,f),h.ra.region&&e.$a.MK(h.ra.region,0,h.ta.z);else if(4===a[1])this.OT(e,h,f),h.ra.road&&e.$a.B8(h.ra.road,h.ta.z);else if(5===a[1]&&h.ra&&h.ra.building)a= g.Nj&&g.Nj.M5(e.e.D),e.Be?h.uf=h.ra.building:(this.OT(e,h,f),e.$a.MK(h.ra.building,1,h.ta.z,void 0,a));else{if(7===a[1]){h.ld&&(e.DL&&0===(h.ta.x+h.ta.y)%2&&h.ld.getContext(\\\"2d\\\").drawImage(e.DL,0,0),h.Bma=!0,g.l.GD&&h.ld.toDataURL&&(f=new Image,f.src=h.ld.toDataURL(),h.ld=f));if(\\\"3D\\\"===(e.e.D&&e.e.D.getViewMode_())){if(h.nl){h.nl.cf=null;h.Vh=null;a=e.e.Hc;if(e.Cf&&a.Cf){c=a.Cf.cI(a.Cf.ca,h.nl);c.mc=a.Cf.mc;a={ni:c,tagName:\\\"CANVAS\\\",width:h.nl.width,height:h.nl.height};c=0;for(f=h.ha.length;c=this.pD(c)&&1>=this.pD(d)?1>=this.qD(c)&&1>=this.qD(d)?\\\"middle\\\":c[1]p){var s=Math.pow(2,p);if(q>=s||0>q)q=(q+s)%s}q=g.a.HI(q,r,p).join(\\\",\\\");g.a.ka(e,q)||e.push(q)}if(this.ay())if(m=this.bfa({Kya:d,Je:c,Od:b,hr:e}),m.LCa=(new Date).getTime(), this.Di)g.Lk.yh(m,function(a,c){if(f.e)if(c.U5){var d=c.Jb.map(function(a){return f.oa.get(a.key)}).filter(function(a){return a});f.Ml(d,b)}else f.Vo?(g.a.Ub(c.Jb,function(a){this.oa.set(a.key,a);a.td&&!a.td.loaded&&(g.jk.vm(a.td),a.td.loaded=!0);a.qe&&a.qe.uf&&g.Mj&&g.Mj.r(\\\"vecTileParsed.buildings\\\",{tp:a})},f),f.set(\\\"display\\\"),f.M1(c,!0)):(g.a.Ub(c.Jb,function(a){var b=this.oa.get(a.key);b&&(b.status=a.status,a.L&&a.L.Ca&&this.hha(a.L.Ca))},f),c.data&&f.Bz(c.data))});else{var u=g.Fc.v6;this.e.D.Ra&& !g.Fc.Yoa&&this.e.D.xq||(u=g.Fc.yh);this.bG();u(m,function(a,c){if(f.e){if(c.U5){var d=c.Jb.map(function(a){return f.oa.get(a.key)}).filter(function(a){return a});f.Ml(d,b)}if(u===g.Fc.v6){if(c.disabled){g.Fc.Yoa=!0;f.set(\\\"display\\\");return}if(c.Ula)return}c.data&&(f.e.Ee!==c.Fu&&(f.e.Ee=c.Fu),g.a.Ub(c.Jb,function(a){var b=this.oa.get(a.key);b&&(b.status=a.status)},f),f.M1(c),c.data&&f.Bz(c.data))}})}else!this.Di&&this.Lc&&this.Lc.V5()?this.e.D.get(\\\"workerMode\\\")?this.RG(d,b,e,c):(1>this.Lc.za.length|| 3=b){var e= this,f=this.e,h=f.D;f.QS||void 0!==f.YS||(\\\"dv\\\"===h.get(\\\"baseRender\\\")?f.YS=1:f.YS=2);d.get({type:h.Ra?\\\"NFS\\\":\\\"FS\\\",Be:this.g.zv||this.g.Be,Fya:a,Nf:!!this.ll,q:this.g,url:this.Ny(b,[]),timeout:1E3},function(d,h){if(e.g){if(d)return c(null,a);h.L6.length&&c(null,h.L6);if(h.lS.length)if(h.lS.length===a.length&&(f.QS&&f.YS--,e.e.D.Ra||e.Di||e.HD||e.e.gU()),h.Pc){e.df=h.Ug[\\\"x-vd-v\\\"];e.Oh=h.Ug.tv;var m=h.Ug.bgc;e.e.Ee!==m&&(e.e.Ee=m);g.a.forEach(h.lS,function(a){a.status=\\\"loaded\\\"});e.G7(h.Pc,b)}else h.r6&& (g.a.forEach(h.r6,function(a){var b=e.oa.get(a.key);if(b){b.status=\\\"loaded\\\";b.qe=a.qe;if(a.td){b.td=a.td;b.ha=a.ha;var c=\\\"limg-\\\"+a.key+\\\"-\\\"+e.g.mc;b.td.Dk=c;b.ha&&(g.a.forEach(b.ha,function(a){a.Ca&&g.a.forEach(a.Ca,function(b,d){b&&0===b.indexOf(\\\"limg-\\\")&&(a.Ca[d]=c)})}),g.jk.vm(b.td),b.td.loaded=!0)}b.qe&&b.qe.uf&&g.Mj&&g.Mj.r(\\\"vecTileParsed.buildings\\\",{tp:b});b.Ba=b.Ra=!0;e.oa.set(a.key,b)}}),e.set(\\\"display\\\"))}})}else c(null,a)},jha:function(a,b,c,d){if(!d||!this.dC()){var e=[(new Date).getTime(), this.Lc.za.length+1&65535,d?1:0].join(\\\"-\\\"),f={command:\\\"tiles\\\",reqId:e,payload:{t:c,opt:b,cs:{level:b,flds:this.g.ha.join(\\\",\\\"),v:g.Ue?\\\"3\\\":\\\"2\\\"}}},h;if(!this.Lc.GJ()){var k=this;setTimeout(function(){k.Lc&&!k.Lc.GJ()&&(k.ru?k.ru++:k.ru=1,k.Lc.zpa(h)&&(delete k.ug[e],k.RG(a,b,c,d)))},300)}h=this.Lc.send(f);this.ug[e]={Jb:a,Od:b}}},qT:function(a){if(!a.Ra||a.Je)a.status=\\\"\\\",a.Ra=void 0,a.Ba=null,a.ld=null,this.oa.Cw(a.key)},bfa:function(a){var b=a.Kya,c=a.Od,d=a.hr;a=a.Je;return{mc:+new Date+\\\"_\\\"+Math.random(), Fu:this.e.Ee,Dd:this.e.Dd,Di:this.Di,Vo:this.Vo,Be:this.g.Be,Hsa:!!g.Ue,Vf:this.J.type,Nf:this.ll,OS:this.g.mc,ha:this.g.ha,Kc:this.g.ja,Ua:this.g.Ua,ZR:Math.ceil(this.rk.Xy),Jb:g.a.filter(b,function(a){return\\\"loaded\\\"!==a.status}),Od:c,hr:d,Je:a,url:this.Ny(c,d,b),Ch:this.bZ(c,[]),VFa:this.e.D.Ra}},bZ:function(a,b){return{AS:\\\"3D\\\"===this.J.type,ZP:this.g.Be,Xr:g.o.Xr||null,hDa:this.g.get(\\\"businessIconsID\\\"),df:this.df,Dd:this.e.Dd,Oh:this.Oh,eFa:this.g.get(\\\"iconsID\\\"),PS:this.g.mc,Gv:a,az:this.g.az, Pf:!this.g.zn()&&this.g.S.get(\\\"merge\\\"),Pc:b,ba:this.g.get(\\\"forceBig\\\")||g.l.ba,Vf:this.J.type,mode:this.g.get(\\\"mode\\\"),lwa:g.o.Cc,G8:this.g.get(\\\"forceBig\\\")?1:this.g.Ua,Sq:this.g.get(\\\"forceBig\\\")?!1:this.g.ja,iy:this.g.get(\\\"forceBig\\\")||g.l.ba&&!this.g.ja?1:0}},cB:function(a){a=a.split(\\\";\\\");for(var b=0,c=a.length;bk&&(f<=e&&d(f+q,h,k),f>=q-e&&d(f-q,h,k));d(f,h,k)},SB:function(a){this.xu||(this.xu=[]);this.xu.push(a)},dk:function(){if(this.xu&&this.xu.length){for(var a=0,b=this.xu.length;af.readyState)){var c=4===f.readyState&&0===f.status;b!==k.gb&&c&&!f.Ot?(f.Ot=!0,k.Ml(a,b),f.onreadystatechange=\\\"\\\",c||f.abort()):f.Ot||(3===f.readyState?(c=f.responseText.substring(h),f.Oq=d(c,f.Oq),h=f.responseText.length): 4===f.readyState&&(c=f.responseText.substring(h),k.Di&&(c+=\\\"|\\\"),d(c,f.Oq),f.Oq=\\\"\\\"))}};f.onerror=function(){};this.Sra||(this.Sra=1);f.open(\\\"GET\\\",e,!0);f.send()},Y9:function(a,b){var c=this.Jw||this.rk,d=a.ta.x,e=a.ta.y;return d>Math.floor(c.Ic/b)||dMath.floor(c.tc/b)||eg.a.Fa/2&&(d.x-=g.a.Fa);this.dQ=d.nd(this.Hg);for(d=c.length-1;0<=d;d-=1){var e=c[d];if(e.length){var f=e[0].ta.z,h=!1;if(e.wg&&!g.Ca.Ke&&(\\\"stable\\\"==this.Hf||f<=this.zoom&&\\\"zoomIn\\\"===this.Hf||f>=this.zoom&&\\\"zoomOut\\\"===this.Hf)&&(!this.labels||!this.labels.length||this.MJ||a.Gi||!this.sE(this.labels.yk,this.rk)|| this.xJ(a)||this.ue!==a.ue)){h=!0;h=[];a.ue&&(h.push.apply(h,a.ue),this.ue=a.ue);a.pb.length?(h.push.apply(h,a.pb),this.QN=a.pb.hI):this.QN=null;for(var k=e.length-1;0<=k;k-=1){var l=e[k],m=!1,n=l.ha;l.se&&(l.se=!1,this.RI(\\\"poilabel\\\",l.ha,void 0,!0),this.Vj.I3(l));!n&&l.qb&&l.qb.ha&&(n=l.qb.ha,m=!0);if(n)for(var p=0,l=n.length;pb.zIndex?-1:1:!a.Ed^!b.Ed?a.Ed?1:-1:a.Ed==b.Ed?a.id==b.id?a.te==b.te?a.nameb.te?-1:1:a.idb.Ed?-1:1});this.labels=e;this.labels.yk=this.rk;this.MJ=!1}}}if(this.gQ(a)){this.Bd();if(!this.e.D.Ra)return;this.mU(a);this.sh=this.labels}this.X6=!1;this.Oe(a)},eT:function(a){this.labels&&this.labels.opacity||(this.labels=[],this.labels.opacity={}); for(var b={},c=0;cg.a.Fa/2&&(b.x-=g.a.Fa);this.transform={translate:new g.H(a.P.Ha.Ac.x*(this.g.ja?this.g.Ua:1),a.P.Ha.Ac.y*(this.g.ja?this.g.Ua:1)),scale:1/(this.g.ja?this.g.Ua:1),rotate:0}},sE:function(a,b){return a.hc===b.hc&&a.Jc===b.Jc&&a.tc===b.tc&&a.Ic===b.Ic},xn:function(a, b){var c;if(this.zoom){var d=a.nd(Math.pow(2,20-this.zoom));this.labels&&(c=this.Qqa(d,b))&&(c.N5=!1);!c&&this.pb&&(c=this.Pqa(d))&&(c.N5=!0)}return c?[c]:[]},Qqa:function(a,b){for(var c=a.x,d=a.y,e=this.labels.IFa||this.labels,f=e.length-1;0<=f;f-=1){var h=e[f],k=h.za[0]/this.T,l=h.za[1]/this.T;if((h.Ys||!b)&&this.J5(h,c,d,k,l))return h}},Pqa:function(a){var b=a.x;a=a.y;for(var c=this.pb.length-1;0<=c;c-=1){var d=this.pb[c],e=d.za[0]/this.T,f=d.za[1]/this.T;if(d.Ys&&this.J5(d,b,a,e,f))return d}}, J5:function(a,b,c,d,e){for(var f=0;f=d+l-1&&b<=d+l+h+1&&c>=e+m-1&&c<=e+m+k+1)return!0}return!1},reloadChanged:function(){this.g&&(this.g.Ra=!1);this.oa.count&&(this.oa.clear(),g.Oj&&g.Oj.clear());this.labels=[];this.l6=[];this.Ob&&this.Ob.parentNode&&this.Ob.parentNode.removeChild(this.Ob);this.$m&&this.$m.parentNode&&this.$m.parentNode.removeChild(this.$m); this.set(\\\"display\\\")},vj:function(){return this.$m},reCalcLabelPos:function(){this.oa&&this.oa.forEach(function(a){a.se=!0})},styleChanged:function(){this.reCalcLabelPos&&this.reCalcLabelPos()},RI:function(a,b,c,d){if(b&&(\\\"roadlabel\\\"===a||\\\"poilabel\\\"===a))for(a=0;aA[0]||256<=A[0]||0>A[1]||256<=A[1])){var B=q.Hz(A,f),A={margin:0,z:m,zIndex:9E3,za:B,name:w[0],Bj:!0,Ta:A,Ca:[],aa:[],visible:!0,xk:!0};A.id=\\\"roadlabel_\\\"+ g.a.zb(A);A.Wq=15>w[2]%90||75A[0]||256<=A[0]||0>A[1]||256<=A[1])){B=q.Hz(A,f);A={id:w[4],margin:n,zIndex:9001,za:B,name:w[0].replace(\\\"^\\\",\\\"\\\"),mk:!0,Ta:A,Ca:[],aa:[],visible:!0,xk:!0};A.id||(A.id=\\\"poilabel_\\\"+g.a.zb(A));P=0;A.Ed=w[8];w[6]&&(I=w[6].split(\\\":\\\"),P=I[1],H=Math.floor((parseInt(I[0].split(\\\"_\\\")[1])-28)/2));if(P&&w[3]){I=g.o.Cc+\\\"://vdata.amap.com/style_icon/icon-\\\"+ (1===H?\\\"biz-\\\":\\\"normal-\\\")+(k||l?\\\"big\\\":\\\"small\\\")+\\\".png\\\";A.Ca.push(I);P=parseInt(P)-1;I=Math.floor(P/10);L=P%10;l?(M=48,K=40,F=28):k?(M=K=40,F=28):(M=K=24,F=20);Q=M;if(151===P||152===P||153===P)F-=4,G=w[7][0][2],Q=M*Math.max(G+2,F)/F;I=[-Q/2,-M/2,Q,M,K*L,K*I,K,K];A.aa.push(I)}if(w[2]&&w[7])for(G=w[2],I=0;Ip&&(c<=m&&f(c+w,n,p),c>=w-m&&f(c-w,n,p));f(c,n,p);return h}};g.M.canvas.rM=g.M.canvas.Qj.extend({ka:[g.sM]});g.M.canvas.Qj.Ib({mU:function(a){this.Wx++;var b=this.$m,c=this.Sk;this.Wy={};var d=this.g.ja?this.g.Ua:1;this.lh=d;var e;0!==a.P.rotation?(e=2*Math.floor(a.P.Ha.Ac.x)*d,d*=2*Math.floor(a.P.Ha.Ac.y)):(e=a.size.width*d,d*=a.size.height);g.f.zm(b,e,d,!0);g.l.Bna&&b.parentNode&&b.parentNode.appendChild(b);c.textBaseline=\\\"bottom\\\";this.gk=0;this.labels&&this.LT(this.labels);this.labels&&this.Twa(this.labels,c,g.Nj.Zqa(this.e.D),this.jd,e,d);this.i6=[];this.labels&&(this.labels.pp=this.zoom,this.labels.op= this.nb,this.labels.Rq=this.rotation,this.labels.NK=this.g.ja,this.labels.size=a.size);a.pb&&(this.pb=a.pb);this.pb&&(this.pb.pp=this.zoom,this.pb.op=this.nb,this.pb.Rq=this.rotation,this.pb.size=a.size);this.r(\\\"afterLabelRender\\\")},Swa:function(a,b,c){var d=void 0===b.xk?!0:b.xk,e=1,f=void 0===b.visible?!0:b.visible,h=null,k=this.e.J.be;if(b.Cm&&k){var l=k=h=\\\"\\\";b.rc&&b.rc.length&&this.LQ&&(l=b.rc[0],h=l[3],k=l[4],l=b.Bj?\\\"\\\":l[7]);if(h=this.J.Py&&this.J.Py(h,k,l,b.Cm,b.Uu))f=h[4],d=h[3],e=h[5]}if(f){if(this.Sk.globalAlpha!== c||e)this.Sk.globalAlpha=c||e;f=0;for(k=b.Ca.length;fg.a.Fa/2?c-=g.a.Fa:c<-g.a.Fa/2&&(c+=g.a.Fa));return this.Mu(c,a[1])},Mu:function(a,b){var c=this.g.ja?this.g.Ua:1,d=this.T,e=0,f=0,e=(a-this.ge[0])/d,f=(b-this.ge[1])/d;return[Math.round(e*c),Math.round(f*c)]},j4:function(a,b){var c=a[0];b&&(c> g.a.Fa/2?c-=g.a.Fa:cb.Xd.x||c[1]>b.Xd.y||c[0]e?(a.opacity[b]-=e,d.ze=1,d.gk=1):a.opacity[b]=0}var d=this,e=0.34;if(!a.qF||\\\"stable\\\"==this.Hf&&\\\"stable\\\"!=this.NS){for(var f=0,h=a.length;fe?(a.opacity[l]-=e,this.gk=this.ze=1):delete a.opacity[l])}a.qF=!0}else if(\\\"zoomIn\\\"== this.Hf){f=0;for(h=a.length;fe?(a.opacity[l]-=e,this.gk=this.ze=1):delete a.opacity[l]);f=0;for(h=a.length;fthis.zoom)for(f=0,h=a.length;fe?(a.opacity[l]-=e,this.gk=this.ze=1):delete a.opacity[l]);else for(f=0,h=a.length;fe?(a.opacity[l]-=e,this.gk=this.ze=1):delete a.opacity[l]);return!1},Twa:function(a,b,c,d,e,f){d=!1;18===a.zoom&&18 l[0]||l[0]>e+256||-256>l[1]||l[1]>f+256||a.opacity[k.id]&&this.Swa(l,k,a.opacity[k.id])}}},xJ:function(a){if(a.pb&&a.pb.length)if(this.pb&&this.pb.length){if(this.pb!==a.pb||this.pb.pp!==this.zoom||this.pb.op!==this.nb||this.pb.Rq!==this.rotation||!this.pb.size.hb(a.size))return!0}else return!0;else if(this.pb&&this.pb.length)return!0},gQ:function(a){this.sh&&this.sh.NK!==this.g.ja&&this.g.set(\\\"reload\\\");return this.gk||this.xJ(a)||this.labels&&(this.e.kR||this.labels!==this.sh||!this.sh||this.sh.pp!== this.zoom||this.sh.op!==this.nb||a.Gi||this.sh.Rq!==this.rotation||this.sh.NK!==this.g.ja||!this.sh.size.hb(a.size))?!0:!1},r5:function(a,b,c){var d,e=b.aa.length,f=[];if(b.Ji>this.zoom)d=!0;else for(var h=0;ha?0:255< a?255:Math.round(a)},A1:function(a,b){for(var c=0;4>c;c++)a[b+c]=this.Dla(a[b+c]);return a},gP:function(a,b){var c=259*(b+255)/(255*(259-b));return[c*(a[0]-128)+128,c*(a[1]-128)+128,c*(a[2]-128)+128]},Yka:function(a){return[255-a[0],255-a[1],255-a[2]]},hP:function(a,b){isNaN(b)&&(b=20);var c=Math.max(0,0.299*a[0]+0.587*a[1]+0.114*a[2]-b);switch(this.Eq){case \\\"amap://styles/normal\\\":case \\\"normal\\\":return[1*c,1.2*c,255];case \\\"amap://styles/light\\\":case \\\"light\\\":return[1*c,1*c,255];case \\\"amap://styles/fresh\\\":case \\\"fresh\\\":return[1* c,1*c,220];case \\\"amap://styles/grey\\\":case \\\"amap://styles/dark\\\":case \\\"insight\\\":case \\\"dark\\\":return[1.5*a[0],1.5*a[1],1*a[2]];case \\\"amap://styles/blue\\\":case \\\"amap://styles/darkblue\\\":case \\\"blue_night\\\":case \\\"mapv\\\":return[2*a[0],1.5*a[1],1*a[2]]}return!1},jga:function(a,b){var c=20,d=10;switch(b.So){case \\\"active\\\":c+=20,d+=20}return this.gP(this.hP(a,c),d)},uZ:function(a,b){var c=-110;switch(b.So){case \\\"active\\\":c+=30}return this.gP(this.hP(this.Yka(a),c),50)},gga:function(a){return this.gP(a,20)},tZ:function(a, b,c,d,e,f){var h=a[b],k=a[b+1],l=a[b+2];a=a[b+3];0b[m+3])){e++;kh[0]&&(h[0]=k);l>h[1]&&(h[1]=l);for(var n=!0,p=0;4>p;p++)if(220>b[m+p]){n=!1;break}n&&d++}}return{hAa:0Math.abs(a[0]-b[0])&&0.1>Math.abs(a[1]-b[1])},sla:function(a,b){for(var c=[a[0]],d=1,e=a.length;d=e&&f<=n)for(e=0;e=e&&f<=n)for(e=c-1;0<=e&&!this.yH(a,f,e,b, c,m,d);e--);}if(!l.length||!m.length)return null;m.reverse();h.reverse();if(this.Px(l[l.length-1],k[0])&&this.Px(k[k.length-1],m[0])&&this.Px(m[m.length-1],h[0])&&this.Px(h[h.length-1],l[0])){b=[m,h,l,k];e=0;for(a=b.length;eb[e].length||(b[e]=this.sla(b[e],d.E6),b[e])))return null;return[].concat(b[0]).concat(b[1].slice(1)).concat(b[2].slice(1)).concat(b[3].slice(1))}return null},Vea:function(a){for(var b=[Infinity,Infinity],c=[-Infinity,-Infinity],d=0,e=a.length;df&&(b[0]=f);b[1]>h&&(b[1]=h);c[0]e?this.vX(a,b,c,d):d&&d.call(this)}},this):d&&d.call(this)},Ida:function(a,b,c){a=a.createImageData(b.width,b.height);var d=a.data,e=a.width,f=a.height,h=c[0]+c[2],k=c[1],l=c[1]+c[3];for(c=c[0];cp;p++)d[n+p]=b.data[n+p];return a},bha:function(a,b,c){var d=a[1]+(b[1]-a[1])*c;return[Math.round(a[0]+(b[0]-a[0])*c),Math.round(d)]},Dga:function(a,b,c){for(var d=1/c,e=[],f=1;fs&&(a<=n&&c(a+q,r,s),a>=q-n&&c(a-q,r,s));c(a,r,s)},Tua:function(a,c){for(var d=a.fF,e=a.Dn,f=[],h=0,k=d.length;hv[0]||256<=v[0]||0>v[1]||256<=v[1])){var u=t.Hz(v,b),v={margin:0,z:f,zIndex:9E3,za:u,name:m[0],Bj:!0,Ta:v,Ca:[],aa:[],visible:!0,xk:!0};v.id=\\\"roadlabel_\\\"+d.zb(v);v.Wq=15>m[2]%90||75v[0]||256<=v[0]||0>v[1]||256<=v[1])){u=t.Hz(v,b);v={id:m[4],margin:e,zIndex:9001,za:u,name:m[0].replace(\\\"^\\\", \\\"\\\"),mk:!0,Ta:v,Ca:[],aa:[],visible:!0,xk:!0};v.id||(v.id=\\\"poilabel_\\\"+d.zb(v));F=0;v.Ed=m[8];m[6]&&(K=m[6].split(\\\":\\\"),F=K[1],L=Math.floor((parseInt(K[0].split(\\\"_\\\")[1])-28)/2));if(F&&m[3]){K=r.Cc+\\\"://vdata.amap.com/style_icon/icon-\\\"+(1===L?\\\"biz-\\\":\\\"normal-\\\")+(s||h?\\\"big\\\":\\\"small\\\")+\\\".png\\\";v.Ca.push(K);F=parseInt(F)-1;K=Math.floor(F/10);Q=F%10;O=J=N=0;h?(N=48,J=40,O=28):s?(N=J=40,O=28):(N=J=24,O=20);V=N;if(151===F||152===F||153===F)O-=4,w=m[7][0][2],V=N*Math.max(w+2,O)/O;K=[-V/2,-N/2,V,N,J*Q,J*K,J,J];2<= F&&5>=F&&(K[8]=!0);v.aa.push(K)}if(m[2]&&m[7])for(w=m[2],K=0;Kw&&(a<=l&&c(f,a+m,v,w),a>=m-l&&c(f,a-m,v,w));c(f,a,v,w)}}},function(a){a||(g.Lk.ota=!0)})}})();(function(){if(!g.Fc){g.Fc=g.KL();var a=g.a.oD({h:\\\"on\\\",we:\\\"hasEvents\\\",G:\\\"off\\\",fK:\\\"offByContext\\\",r:\\\"emit\\\",ui:\\\"clearEvents\\\"});g.Fc.xf(null,function(){return{usa:function(){var a=this;this.as=[];this.bs={};if(\\\"undefined\\\"!==typeof worker)for(var c=0;10>c;c++){var d=new Worker(this.yna);d.status={index:c,m5:!0,DK:!1,hL:0};d.addEventListener(\\\"message\\\",function(c){var d=c.data;c=c.target;d.ping&&200===d.status?(c.status.DK=!0,a.as.filter(function(a){return a.status.DK}).length===a.as.length&&(a.as.DK=!0)): d.result&&a.kwa(c,d)});this.as.push(d);d.postMessage({cmd:\\\"ping\\\",ts:(new Date).getTime()})}},kwa:function(a,c){var d=c.parentTaskId,e=c.taskId,f=this.bs[d],h=!1;f[e].data=c;f[e].index=a.status.index;for(var k=0,l=f.length;kdiv,.amap-zoom-touch-minus>div{margin:auto;font-size:26px;line-height:43px;font-family:verdana;text-align:center;color:#666;height:100%;cursor:pointer}.amap-zoom-touch>div{opacity:.2}', A:function(a){this.CLASS_NAME=\\\"AMap.ToolBar\\\";g.c.ya(this,a);this.Ug(a||{});this.j={};this.j.K=g.f.create(\\\"div\\\",null,\\\"amap-toolbar\\\");this.j.Qv=g.f.create(\\\"div\\\",this.j.K,\\\"amap-pancontrol\\\");this.j.left=g.f.create(\\\"div\\\",this.j.Qv,\\\"amap-pan-left\\\");this.j.top=g.f.create(\\\"div\\\",this.j.Qv,\\\"amap-pan-top\\\");this.j.right=g.f.create(\\\"div\\\",this.j.Qv,\\\"amap-pan-right\\\");this.j.bottom=g.f.create(\\\"div\\\",this.j.Qv,\\\"amap-pan-bottom\\\");this.j.ff=g.f.create(\\\"div\\\",this.j.K,\\\"amap-locate\\\");this.j.zoom=g.f.create(\\\"div\\\",this.j.K, \\\"amap-zoomcontrol\\\");this.j.sk=g.f.create(\\\"div\\\",this.j.zoom,\\\"amap-zoom-plus\\\");this.j.Uh=g.f.create(\\\"div\\\",this.j.zoom,\\\"amap-zoom-ruler\\\");this.j.zf=g.f.create(\\\"div\\\",this.j.zoom,\\\"amap-zoom-minus\\\");this.j.Oc=g.f.create(\\\"div\\\",this.j.Uh,\\\"amap-zoom-mask\\\");this.j.cursor=g.f.create(\\\"div\\\",this.j.Uh,\\\"amap-zoom-cursor\\\");this.j.labels=g.f.create(\\\"div\\\",this.j.Uh,\\\"amap-zoom-labels\\\");this.j.street=g.f.create(\\\"div\\\",this.j.labels,\\\"amap-zoom-label-street\\\");this.j.city=g.f.create(\\\"div\\\",this.j.labels,\\\"amap-zoom-label-city\\\"); this.j.g8=g.f.create(\\\"div\\\",this.j.labels,\\\"amap-zoom-label-province\\\");this.j.country=g.f.create(\\\"div\\\",this.j.labels,\\\"amap-zoom-label-country\\\");(this.Or=(this.Ala=g.l.ba)||a&&a.liteStyle)?(this.Sxa(),this.position=\\\"RB\\\",this.offset=new g.H(10,110)):g.f.ab(this.j.Qv,{position:\\\"relative\\\"}).ab(this.j.ff,{position:\\\"relative\\\",left:\\\"17px\\\"}).ab(this.j.zoom,{position:\\\"relative\\\",left:\\\"14px\\\"});this.vb(a);this.LP&&this.yP()},Ug:function(a){this.Oza=a.useNative||!1;this.dj=\\\"toolbar\\\";this.m9=this.ff=this.direction= this.Uh=this.visible=!0;this.position=\\\"LT\\\";this.offset=new g.H(10,10);this.LP=!1;this.timeout=a.timeout||5E3;this.Tta=g.o.Fb+\\\"/theme/v1.3/small_loading.gif\\\";this.Bq=new z.B.wb({position:this.Hh,offset:new g.H(-11,-11),innerOverlay:!0,content:\\\"
\\\"});this.Bq.Da=!0},zu:function(a,b){this.map=a;b.appendChild(this.j.K);this.vf=b;this.O1();this.fn(); var c=this;setTimeout(function(){c.map&&c.l0()},0)},Yv:function(){this.mn();this.vf.removeChild(this.j.K);this.map=this.vf=null;this.xP()},Sxa:function(){var a=g.f.create(\\\"div\\\",this.j.sk);a.innerHTML=\\\"+\\\";g.f.fb(this.j.sk,\\\"amap-zoom-plus\\\");g.f.Wa(this.j.sk,\\\"amap-zoom-touch-plus\\\");g.f.fb(this.j.ff,\\\"amap-locate\\\");g.f.Wa(this.j.ff,\\\"amap-geo\\\");g.f.Wa(this.j.ff,\\\"amap-toolbar-geo\\\");a=g.f.create(\\\"div\\\",this.j.zf);a.innerHTML=\\\"−\\\";g.f.fb(this.j.zf,\\\"amap-zoom-minus\\\");g.f.Wa(this.j.zf,\\\"amap-zoom-touch-minus\\\"); g.f.Wa(this.j.K,\\\"amap-touch-toolbar\\\")},Txa:function(a){a.direction=!1;!0!==a.locate&&(a.locate=!1);a.ruler=!1;return a},vb:function(a){a=a||{};this.Or&&(a=this.Txa(a));for(var b in a)a.hasOwnProperty(b)&&\\\"_\\\"!==b.substr(0,1)&&void 0!==this[b]&&(this[b]=a[b]);this.offset=void 0!==a.offset?a.offset:this.offset;this.position=void 0!==a.position?a.position:this.position;this.Uh=void 0!==a.ruler?a.ruler:this.Uh;this.ff=void 0!==a.locate?a.locate:this.ff;this.direction=void 0!==a.direction?a.direction:this.direction; this.LP=void 0!==a.autoPosition?a.autoPosition:this.LP;void 0!==a.locationMarker&&(this.Bq.getMap===this.map&&(this.Bq.setMap(null),a.locationMarker.setPosition(this.Bq.getPosition()),a.locationMarker.setMap(this.map)),this.Bq=a.locationMarker);this.mha();this.position&&this.offset&&this.cC(this.position,this.offset);this.direction?g.f.ab(this.j.Qv,{display:\\\"block\\\"}):g.f.ab(this.j.Qv,{display:\\\"none\\\"});this.ff?g.f.ab(this.j.ff,{display:\\\"block\\\"}):g.f.ab(this.j.ff,{display:\\\"none\\\"});!this.Uh||this.map&& 270>this.map.getSize().getHeight()?g.f.ab(this.j.Uh,{display:\\\"none\\\"}):g.f.ab(this.j.Uh,{display:\\\"block\\\"});this.visible?g.f.ab(this.j.K,{visibility:\\\"visible\\\"}):g.f.ab(this.j.K,{visibility:\\\"hidden\\\"})},IR:function(a){return this[a]},cC:function(a,b){switch(a){case \\\"LT\\\":g.f.ab(this.j.K,{left:b.wf()+\\\"px\\\",top:b.ve()+\\\"px\\\",right:\\\"\\\",bottom:\\\"\\\"});break;case \\\"RT\\\":g.f.ab(this.j.K,{right:b.wf()+\\\"px\\\",top:b.ve()+\\\"px\\\",left:\\\"\\\",bottom:\\\"\\\"});break;case \\\"LB\\\":g.f.ab(this.j.K,{left:b.wf()+\\\"px\\\",bottom:b.ve()+\\\"px\\\",right:\\\"\\\", top:\\\"\\\"});break;case \\\"RB\\\":g.f.ab(this.j.K,{right:b.wf()+\\\"px\\\",bottom:b.ve()+\\\"px\\\",left:\\\"\\\",top:\\\"\\\"})}this.vf&&(this.vf.removeChild(this.j.K),this.vf.appendChild(this.j.K))},l0:function(){this.Za=null;this.O1();this.mP(this.map.get(\\\"zoom\\\"))},fn:function(){this.mP(this.map.get(\\\"zoom\\\"));this.Va={Za:g.event.addListener(this.map,\\\"zooms\\\",this.l0,this),left:g.event.Y(this.j.left,\\\"click\\\",this.Zha,this),top:g.event.Y(this.j.top,\\\"click\\\",this.Pia,this),right:g.event.Y(this.j.right,\\\"click\\\",this.via,this),bottom:g.event.Y(this.j.bottom, \\\"click\\\",this.Jha,this),ff:g.event.Y(this.j.ff,\\\"click\\\",this.bia,this),BGa:g.event.Y(this.j.K,\\\"mousedown\\\",function(a){g.F.preventDefault(a)},this),NDa:g.event.Y(this.j.cursor,\\\"mousedown\\\",this.gu,this),DDa:g.event.Y(this.j.cursor,\\\"mouseover\\\",this.Uha,this),CDa:g.event.Y(this.j.cursor,\\\"mouseout\\\",this.Vha,this),LDa:g.event.Y(document,\\\"mousemove\\\",this.eu,this),MDa:g.event.Y(document,\\\"mouseup\\\",this.fu,this),cursor:g.event.Y(this.j.cursor,\\\"click\\\",this.Tha,this),sk:g.event.Y(this.j.sk,\\\"click\\\",this.sia,this), zf:g.event.Y(this.j.zf,\\\"click\\\",this.dia,this),wHa:g.event.Y(this.j.Uh,\\\"mouseover\\\",this.Y_,this),PIa:g.event.Y(this.j.zoom,\\\"mouseout\\\",this.X_,this),GFa:g.event.Y(this.j.labels,\\\"mouseover\\\",this.Y_,this),FFa:g.event.Y(this.j.labels,\\\"mouseout\\\",this.X_,this),Uh:g.event.Y(this.j.Uh,\\\"click\\\",this.Nia,this),street:g.event.Y(this.j.street,\\\"click\\\",this.Oia,this),city:g.event.Y(this.j.city,\\\"click\\\",this.Nha,this),g8:g.event.Y(this.j.g8,\\\"click\\\",this.tia,this),country:g.event.Y(this.j.country,\\\"click\\\",this.Sha,this), V$:g.event.addListener(this.map,\\\"zoomend\\\",this.po,this),gIa:g.event.Y(this.j.K,\\\"touchstart\\\",g.a.C3,g.F.stopPropagation),resize:g.event.addListener(this.map,\\\"resize\\\",this.uia,this)};g.l.Ai&&(this.Va.KFa=g.event.Y(this.j.left,\\\"mouseover\\\",this.aia,this),this.Va.eIa=g.event.Y(this.j.top,\\\"mouseover\\\",this.Ria,this),this.Va.hHa=g.event.Y(this.j.right,\\\"mouseover\\\",this.yia,this),this.Va.bDa=g.event.Y(this.j.bottom,\\\"mouseover\\\",this.Lha,this),this.Va.JFa=g.event.Y(this.j.left,\\\"mouseout\\\",this.$ha,this),this.Va.dIa= g.event.Y(this.j.top,\\\"mouseout\\\",this.Qia,this),this.Va.gHa=g.event.Y(this.j.right,\\\"mouseout\\\",this.xia,this),this.Va.aDa=g.event.Y(this.j.bottom,\\\"mouseout\\\",this.Kha,this))},Uha:function(){this.j.cursor.style.top=parseInt(this.j.cursor.style.top)-2+\\\"px\\\"},Vha:function(){this.j.cursor.style.top=parseInt(this.j.cursor.style.top)+2+\\\"px\\\"},mn:function(){if(this.Va)for(var a in this.Va)this.Va.hasOwnProperty(a)&&g.event.removeListener(this.Va[a])},Zha:function(){this.map.panBy(this.map.getSize().width/2-1, 0)},Jha:function(){this.map.panBy(0,-this.map.getSize().height/2+1)},via:function(){this.map.panBy(-this.map.getSize().width/2+1,0)},aia:function(){this.j.left.className+=\\\" amap-pan-left-hover\\\"},yia:function(){this.j.right.className+=\\\" amap-pan-right-hover\\\"},Ria:function(){this.j.top.className+=\\\" amap-pan-top-hover\\\"},Lha:function(){this.j.bottom.className+=\\\" amap-pan-bottom-hover\\\"},$ha:function(){this.j.left.className=this.j.left.className.replace(\\\" amap-pan-left-hover\\\",\\\"\\\")},xia:function(){this.j.right.className= this.j.right.className.replace(\\\" amap-pan-right-hover\\\",\\\"\\\")},Qia:function(){this.j.top.className=this.j.top.className.replace(\\\" amap-pan-top-hover\\\",\\\"\\\")},Kha:function(){this.j.bottom.className=this.j.bottom.className.replace(\\\" amap-pan-bottom-hover\\\",\\\"\\\")},Pia:function(){this.map.panBy(0,this.map.getSize().height/2-1)},mha:function(){navigator.geolocation||(this.ff=!1)},O1:function(){this.Za||(this.Za=this.map?this.map.get(\\\"zooms\\\"):[3,18]);this.m9=5this.Za[1]?!1:!0},bia:function(){this.Hh? this.xP():this.yP()},GCa:function(a,b){var c=g.o.fd+\\\"/v3/assistant/coordinate/convert?coordsys=gps&s=rsv3&output=json\\\",c=c+\\\"&locations=\\\"+a.toString()+\\\"&key=\\\"+g.o.key;g.o.Xa&&(c+=\\\"&jscode=\\\"+g.o.Xa);c=new g.kb.Ab(c,{callback:\\\"callback\\\"});c.h(\\\"complete\\\",b,this);c.h(\\\"error\\\",b,this)},yP:function(){if(navigator.geolocation){this.Or||(this.j.Ke||(this.j.Ke=g.f.create(\\\"img\\\",this.j.ff),this.j.Ke.src=this.Tta,g.f.ab(this.j.Ke,{margin:(this.Or?10:4)+\\\"px\\\"})),this.j.ff.appendChild(this.j.Ke));g.f.Wa(this.j.ff, \\\"amap-locate-loading\\\");var a=this;g.ub.load(\\\"AMap.Geolocation\\\",function(){a.geolocation||(a.geolocation=new g.Vn({useNative:this.Oza,showButton:!1,showMarker:!1,showCircle:!1,panToLocation:!1,maximumAge:6E5}));a.geolocation.getCurrentPosition(function(b,c){g.f.fb(a.j.ff,\\\"amap-locate-loading\\\");\\\"complete\\\"===b?(a.nha(c),g.event.O(a,\\\"location-success\\\",c)):(a.lha(),g.event.O(a,\\\"location-failed\\\",c))})})}},xP:function(){this.Or?g.f.fb(this.j.ff,\\\"amap-toolbar-geo-secc\\\"):g.f.ab(this.j.ff,{backgroundPosition:\\\"-130px -185px\\\"}); this.dga();this.Hh=null},qla:function(){var a=this;if(this.Hh)if(this.Tb=this.Bq,this.Tb.setMap(this.map),this.Bq instanceof z.B.wb&&this.Bq.setPosition(this.Hh),this.MA&&(this.Qa=new z.B.Ze({isCustom:!0,innerOverlay:!0,content:\\\"\\\\u5b9a\\\\u4f4d\\\\u7cbe\\\\u5ea6:\\\"+this.MA+\\\"\\\\u7c73\\\",offset:new g.H(25,0)}),this.Tb.h(\\\"mouseover\\\",function(){a.Qa.open(a.map,a.Tb.getPosition())}),this.Tb.h(\\\"mouseout\\\", function(){a.Qa.close()})),this.MA){this.Lb=new z.B.hh({center:this.Hh,radius:this.MA,strokeColor:\\\"#0093FF\\\",strokeOpacity:0.3,strokeWeight:1,fillColor:\\\"#02B0FF\\\",fillOpacity:0.25,innerOverlay:!0});this.Lb.Da=!0;this.Lb.setMap(this.map);var b=this.map.getSize(),b=Math.min(b.width,b.height)/4,b=Math.floor(Math.LOG2E*Math.log(b*Math.cos(this.Hh.getLat()*Math.PI/180)*12756274*Math.PI/256/this.MA));this.map.setZoomAndCenter(b,this.Hh)}else this.map.setCenter(this.Hh);g.event.O(this,\\\"location\\\",{type:\\\"location\\\", lnglat:this.Hh})},dga:function(){this.Tb&&(this.Tb.setMap(null),this.Tb=null);this.Lb&&(this.Lb.setMap(null),this.Lb=null);this.Bq.setMap(null)},pla:function(){this.m9&&this.j.labels&&g.f.ab(this.j.labels,{display:\\\"block\\\"})},cga:function(){this.j.labels&&g.f.ab(this.j.labels,{display:\\\"none\\\"})},rA:function(a,b){var c=b-a;c&&g.event.O(this,\\\"zoomchanged\\\",{type:0===c?void 0:0c[1]&&(a=c[1]);ab-10&&(a=b-10);g.f.ab(this.j.cursor,{top:a+\\\"px\\\"}).ab(this.j.Oc,{height:a+\\\"px\\\"});a=this.map.get(\\\"zoom\\\");b=this.mZ();this.map.setZoom(b);this.rA(a,b)}},Oia:function(a){this.map&&this.map.getStatus().zoomEnable&&(g.F.stopPropagation(a).preventDefault(a), this.map.setZoom(17),a=this.map.get(\\\"zoom\\\"),this.rA(a,17))},Nha:function(a){this.map&&this.map.getStatus().zoomEnable&&(g.F.stopPropagation(a).preventDefault(a),this.map.setZoom(11),a=this.map.get(\\\"zoom\\\"),this.rA(a,11))},tia:function(a){this.map&&this.map.getStatus().zoomEnable&&(g.F.stopPropagation(a).preventDefault(a),this.map.setZoom(7),a=this.map.get(\\\"zoom\\\"),this.rA(a,7))},Sha:function(a){this.map&&this.map.getStatus().zoomEnable&&(g.F.stopPropagation(a).preventDefault(a),this.map.setZoom(3), a=this.map.get(\\\"zoom\\\"),this.rA(a,3))},uia:function(){this.vb({locate:this.ff})},nha:function(a){try{this.Hh=a.position,this.MA=a.accuracy,this.j.Ke&&this.j.Ke.parentNode&&this.j.Ke.parentNode.removeChild(this.j.Ke),this.Or?g.f.Wa(this.j.ff,\\\"amap-toolbar-geo-secc\\\"):g.f.ab(this.j.ff,{backgroundPosition:\\\"-130px -211px\\\"}),this.qla({locations:this.Hh+\\\"\\\"})}catch(b){}},lha:function(){this.Hh=null;try{this.j.ff.removeChild(this.j.Ke),this.Or||g.f.ab(this.j.ff,{backgroundPosition:\\\"-130px -185px\\\"})}catch(a){}}, getOffset:function(){g.c.add(this,\\\"getOffset\\\");return this.offset},setOffset:function(a){g.c.add(this,\\\"setOffset\\\");this.vb({offset:a})},hideRuler:function(){g.c.add(this,\\\"hideRuler\\\");this.vb({ruler:!1})},showRuler:function(){g.c.add(this,\\\"showRuler\\\");this.vb({ruler:!0})},hideDirection:function(){g.c.add(this,\\\"hideDirection\\\");this.vb({direction:!1})},showDirection:function(){g.c.add(this,\\\"showDirection\\\");this.vb({direction:!0})},hideLocation:function(){g.c.add(this,\\\"hideLocation\\\");this.vb({locate:!1})}, showLocation:function(){g.c.add(this,\\\"showLocation\\\");this.vb({locate:!0})},hide:function(){g.c.add(this,\\\"hide\\\");this.vb({visible:!1});g.event.O(this,\\\"hide\\\")},show:function(){g.c.add(this,\\\"show\\\");this.vb({visible:!0});g.event.O(this,\\\"show\\\")},doLocation:function(){g.c.add(this,\\\"doLocation\\\");this.Hh&&this.xP();this.yP()},getLocation:function(){g.c.add(this,\\\"getLocation\\\");return this.Hh},Ty:function(){var a=[0,0,0,0];if(this.j.K){var b=g.f.$c(this.j.K,\\\"width\\\"),c=g.f.$c(this.j.K,\\\"height\\\"),b=this.offset.wf()+ parseFloat(b),c=this.offset.ve()+parseFloat(c)+5;-1!==this.position.indexOf(\\\"T\\\")&&(a[0]=c);-1!==this.position.indexOf(\\\"R\\\")&&(a[1]=b);-1!==this.position.indexOf(\\\"B\\\")&&(a[2]=c);-1!==this.position.indexOf(\\\"L\\\")&&(a[3]=b)}return a}});window.AMap.ToolBar||(window.AMap.ToolBar=g.Bba); \"}"}, {"name": "_AMap_AMap.MapType", "value": "{\"version\":\"1768801478717\",\"script\":\"g.Uaa=g.da.extend({Ni:\\\".amap-maptypecontrol{z-index:150;position:absolute;top:12px;right:12px;z-index:304}.amap-maptype-wrap{position:absolute;top:0;right:0;width:64px;height:64px}.amap-maptype-con{position:absolute;top:0;right:0;width:60px;height:60px;background:#fff;border:solid 1px #ccc;cursor:pointer}.amap-maptype-win{position:absolute;top:1px;right:1px;width:56px;height:56px;overflow:hidden;background:#ccc;border:solid 1px #ccc}.amap-maptype-title{position:absolute;bottom:0;left:0;width:60px;height:20px;color:#333;text-align:center;font-size:12px;line-height:20px;background:#fff}.amap-maptype-title:hover{background-color:#eee}.amap-maptype-list{position:absolute;top:64px;right:0;width:100px;background:#fff;border:solid 1px #ccc;overflow:hidden}.amap-maptype-list p{width:100px;height:20px;margin:0;cursor:pointer;background:#fff}.amap-maptype-lsit p:hover{background:#eee}.amap-maptype-check{display:inline-block;width:15px;height:15px;margin-left:2px;background:url(../../theme/v1.3/maptype-bg.png) no-repeat -30px 4px}.amap-maptype-checked{background-position:0 4px}.amap-maptype-check-hover{background-position:-15px 4px}.amap-maptype-label{display:inline-block;width:60px;height:15px;color:#333;font-size:12px;line-height:15px;margin-left:10px}\\\", ka:[g.va],A:function(a){this.CLASS_NAME=\\\"AMap.MapType\\\";g.c.ya(this,a);this.d3=0;this.p9=this.Uz=!1;this.visible=!0;a&&(\\\"undefined\\\"!==typeof a.defaultType&&(this.d3=a.defaultType),\\\"undefined\\\"!==typeof a.showTraffic&&(this.Uz=a.showTraffic),\\\"undefined\\\"!==typeof a.showRoad&&(this.p9=a.showRoad));this.Os=[{title:\\\"\\\\u5730\\\\u56fe\\\",q:new z.q.mb({innerLayer:!0}),lf:[\\\"traffic\\\"]},{title:\\\"\\\\u536b\\\\u661f\\\",q:new z.q.mb.JW({innerLayer:!0}),lf:[\\\"traffic\\\",\\\"road\\\"]}];this.lf={traffic:{title:\\\"\\\\u8def\\\\u51b5\\\",q:new z.q.mb.RW({innerLayer:!0}), show:this.Uz},road:{title:\\\"\\\\u8def\\\\u7f51\\\",q:new z.q.mb.GW({innerLayer:!0}),show:this.p9}};this.roa()},zu:function(a,b){this.map=a;this.vf=b;b.appendChild(this.j.K);this.fn();this.b1(this.d3);this.$X()},Yv:function(){this.mn();this.vf.removeChild(this.j.K);this.map=this.vf=null},roa:function(){this.ks=-1;var a={};a.K=g.f.create(\\\"div\\\",null,\\\"amap-maptypecontrol\\\");a.Cl=g.f.create(\\\"div\\\",a.K,\\\"amap-maptype-wrap\\\");a.uQ=g.f.create(\\\"div\\\",a.Cl,\\\"amap-maptype-con\\\");a.OV=g.f.create(\\\"div\\\",a.uQ,\\\"amap-maptype-win\\\"); a.title=g.f.create(\\\"div\\\",a.uQ,\\\"amap-maptype-title\\\");a.list=g.f.create(\\\"div\\\",a.K,\\\"amap-maptype-list\\\");for(var b in this.lf)if(this.lf.hasOwnProperty(b)){var c=g.f.create(\\\"p\\\",a.list,\\\"amap-maptype-item\\\");c.qy=g.f.create(\\\"span\\\",c,\\\"amap-maptype-check\\\");this.lf[b].show&&g.f.Wa(c.qy,\\\"amap-maptype-checked\\\");c.kta=g.f.create(\\\"span\\\",c,\\\"amap-maptype-label\\\");c.kta.innerHTML=this.lf[b].title;this.lf[b].ce=c}a.list.style.width=\\\"60px\\\";this.j=a},SX:function(){for(var a in this.lf)if(this.lf.hasOwnProperty(a)){var b= this.lf[a],c=b.q.get(\\\"zooms\\\"),d=this.map.getZoom();b.map=this.map;b.ce.title=d>c[1]||de?c-5:b.mN,f=5f?d-5:b.lN,b.j.list.style.width=e+\\\"px\\\",b.j.list.style.height=f+\\\"px\\\"):(clearInterval(b.fO),a&&a.call(b))},50)},m1:function(){this.fO&&clearInterval(this.fO)},o4:function(a){var b=[];if(this.Os[a]){b.push(this.Os[a].q);a=this.Os[a].lf;a=g.a.isArray(a)?a.join(): \\\"\\\";for(var c in this.lf)this.lf.hasOwnProperty(c)&&-1!==a.indexOf(c)&&this.lf[c].show&&b.push(this.lf[c].q)}return b},b1:function(a){a!==this.ks&&(this.map.remove(this.o4(this.ks)),-1==this.ks&&this.map.remove(this.map.getDefaultLayer()),this.map.add(this.o4(a)),this.ks=a,this.kH(),this.Kka())},kH:function(){var a=this.Os[(this.ks+1)%this.Os.length];this.j.title.innerHTML=a.title;var b=Number(g.f.$c(this.j.OV,\\\"width\\\").replace(\\\"px\\\",\\\"\\\")),c=Number(g.f.$c(this.j.OV,\\\"height\\\").replace(\\\"px\\\",\\\"\\\"))||56,d=this.map.getSize().width, e=this.map.getSize().height;if(!(0>=b||0>=c||0>=d||0>=e)){var f=this.map.lc(this.map.getCenter()),f=f.add(new g.H(d/2-14-b/2,14+c/2-e/2)),f=this.map.Xh(f);this.Pka(this.j.OV,f,this.map,a.q,b,c)}},Kka:function(){this.m1();var a=this.Os[this.ks].lf;if(0===a.length)this.j.list.style.display=\\\"none\\\";else{this.j.list.style.display=\\\"block\\\";this.j.list.style.height=20*a.length+\\\"px\\\";var a=a.join(\\\",\\\"),b;for(b in this.lf)this.lf.hasOwnProperty(b)&&(-1!==a.indexOf(b)?this.lf[b].ce.style.display=\\\"block\\\":this.lf[b].ce.style.display= \\\"none\\\")}},Pka:function(a,b,c,d,e,f){a.Yk||(a.Yk=[]);var h=d.getZooms(),k=Math.ceil(c.getZoom()),k=Math.min(Math.max(k,h[0]),h[1]);b=c.lc(b,k);c=Math.floor((b.y-f/2)/256);for(var h=Math.floor((b.x+e/2)/256),l=Math.floor((b.y+f/2)/256),m=0,n=Math.floor((b.x-e/2)/256);n<=h;n+=1)for(var p=c;p<=l;p+=1){a.Yk.length<=m&&a.Yk.push(g.f.create(\\\"img\\\",a));var q=a.Yk[m];q.style.visibility=\\\"inherit\\\";q.style.position=\\\"absolute\\\";q.style.left=256*n-b.x+e/2+\\\"px\\\";q.style.top=256*p-b.y+f/2+\\\"px\\\";var r=d.getTileUrl(n, p,k);q.src=r;m+=1}for(;m=a?\\\"en\\\"==this.map.getLang()?c+\\\" KM\\\":\\\"zh_en\\\"==this.map.getLang()?c+\\\" \\\\u516c\\\\u91cc/KM\\\":c+\\\" \\\\u516c\\\\u91cc\\\":\\\"en\\\"==this.map.getLang()?c+\\\" M\\\":\\\"zh_en\\\"==this.map.getLang()?c+\\\" \\\\u7c73/M\\\":c+\\\" \\\\u7c73\\\";this.jla(c,b)},show:function(){g.c.add(this,\\\"show\\\");this.vb({visible:!0});g.event.O(this,\\\"show\\\")},hide:function(){g.c.add(this,\\\"hide\\\");this.vb({visible:!1}); g.event.O(this,\\\"hide\\\")},Ty:function(){var a=[0,0,0,0];if(this.j.K){var b=g.f.$c(this.j.K,\\\"width\\\"),c=g.f.$c(this.j.K,\\\"height\\\"),b=this.offset.wf()+parseFloat(b),c=this.offset.ve()+parseFloat(c);-1!==this.position.indexOf(\\\"T\\\")&&(a[0]=c);-1!==this.position.indexOf(\\\"R\\\")&&(a[1]=b);-1!==this.position.indexOf(\\\"B\\\")&&(a[2]=c);-1!==this.position.indexOf(\\\"L\\\")&&(a[3]=b)}return a}});window.AMap.Scale=g.tba; \"}"}]}]} \ No newline at end of file diff --git a/tests/web/conftest.py b/tests/web/conftest.py deleted file mode 100644 index c4d615f..0000000 --- a/tests/web/conftest.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -Web 端根级 fixture:浏览器实例 + 登录态复用 + 失败自动截图。 - -核心机制 — 跳过图形验证码: - 配置 web.skip_login: true 时,读取已保存的 storage_state(cookies + localStorage), - 跳过 UI 登录步骤。首次需要手动登录一次来生成 auth 文件。 - -首次初始化流程: - 1. python tests/web/save_auth.py - 2. 打开的浏览器中手动完成验证码登录 - 3. 登录态自动保存到 tests/web/auth/.json - -后续用例执行时自动复用,过期后重新运行 save_auth.py 即可。 - -提供: - browser_name — 参数化 fixture,每个浏览器执行一轮 - page — Playwright Page 对象(已加载登录态) - ensure_login — 确保指定角色的登录态有效,未登录则报错提示 -""" - -from datetime import datetime -from pathlib import Path - -import pytest -from playwright.sync_api import sync_playwright, Page, BrowserContext - -SCREENSHOTS_DIR = Path("output/screenshots") -AUTH_DIR = Path(__file__).parent / "auth" - - -@pytest.fixture(scope="session") -def web_config(config): - """session 级:Web 配置段。""" - return config["web"] - - -@pytest.fixture(params=["chromium", "firefox"]) -def browser_name(web_config): - """每个浏览器执行一次同一套用例。 - - 可从命令行覆盖: pytest --browser=chromium tests/web/ - """ - return request.param - - -@pytest.fixture -def page(web_config, browser_name, request) -> Page: - """每个用例独立的 Playwright Page,自动加载登录态,失败自动截图。""" - viewport = web_config.get("viewport", {"width": 1920, "height": 1080}) - timeout = web_config.get("timeout_seconds", 120) * 1000 - screenshot_on_failure = web_config.get("screenshot_on_failure", True) - skip_login = web_config.get("skip_login", True) - storage_state_dir = Path(web_config.get("storage_state_dir", "tests/web/auth")) - - with sync_playwright() as p: - browser_launcher = getattr(p, browser_name) - browser = browser_launcher.launch(headless=True) - - # ── 构建 context ── - context_kwargs = { - "viewport": {"width": viewport["width"], "height": viewport["height"]}, - "locale": "zh-CN", - } - - # 如果启用跳过登录,尝试加载 storage_state - loaded_role = None - if skip_login: - loaded_role = _load_auth_state(context_kwargs, storage_state_dir, browser_name) - - context = browser.new_context(**context_kwargs) - page = context.new_page() - page.set_default_timeout(timeout) - - # 将当前 context 的登录角色注入 page(用例中可检查) - page._auth_role = loaded_role - - yield page - - # ── 失败自动截图 ── - if screenshot_on_failure and hasattr(request.node, "rep_call") and request.node.rep_call.failed: - scenario_dir = SCREENSHOTS_DIR / request.node.name - scenario_dir.mkdir(parents=True, exist_ok=True) - ts = datetime.now().strftime("%Y%m%d_%H%M%S") - path = str(scenario_dir / f"FAIL_{browser_name}_{ts}.png") - page.screenshot(path=path, full_page=True) - - context.close() - browser.close() - - -@pytest.fixture -def ensure_login(web_config, page, config): - """确保已登录状态,未登录则自动执行 UI 登录(含验证码则报错)。 - - 用法一(推荐 — 配置 skip_login: true): - def test_xxx(page, ensure_login): - ensure_login("super_admin") - # page 已处于登录后的管理后台 - - 用法二(skip_login: false — 完整 UI 登录): - def test_xxx(page, ensure_login): - ensure_login("super_admin") # 执行完整 UI 登录 - """ - - def _ensure_login(role: str = "super_admin") -> None: - account = config["accounts"].get(role) - if not account: - raise ValueError(f"未知角色 '{role}',可用: {list(config['accounts'].keys())}") - - base_url = web_config["base_url"] - skip_login = web_config.get("skip_login", True) - - # 如果 storage_state 已加载且角色匹配,直接验证 - if skip_login: - auth_role = getattr(page, "_auth_role", None) - if auth_role == role: - # 验证登录态是否仍然有效 - page.goto(base_url) - # Hash 路由模式下判断是否重定向到登录页 - page.wait_for_timeout(2000) # 等 hash 路由完成 - if "/#/login" not in page.url and "/login" not in page.url: - return # 登录态有效 - # 登录态过期,给出明确指引 - raise RuntimeError( - f"登录态已过期或未生成!请运行以下命令重新生成:\n" - f" python tests/web/save_auth.py --role {role} --browser {_get_browser_name(page)}" - ) - - if auth_role and auth_role != role: - raise RuntimeError( - f"当前 storage_state 角色为 '{auth_role}',请求为 '{role}'。\n" - f"请运行: python tests/web/save_auth.py --role {role}" - ) - - # 没有加载任何 auth,报错 - raise RuntimeError( - f"未找到角色 '{role}' 的登录态文件!请运行:\n" - f" python tests/web/save_auth.py --role {role}" - ) - - # ── skip_login: false,执行 UI 登录(可能有验证码,会失败) ── - page.goto(f"{base_url}/#/login") - page.get_by_placeholder("请输入用户名").fill(account["phone"]) - page.get_by_placeholder("请输入密码").fill(account["password"]) - page.get_by_role("button", name="登录").click() - - # 等几秒让页面处理路由跳转 - page.wait_for_timeout(3000) - if "/#/login" in page.url or "/login" in page.url: - raise RuntimeError( - "登录失败!可能存在图形验证码。\n" - "请改为手动登录生成 auth 文件: python tests/web/save_auth.py --role {role}" - ) - - -def _load_auth_state(context_kwargs: dict, storage_dir: Path, browser: str) -> str | None: - """尝试加载登录态,返回角色名或 None。""" - auth_files = list(storage_dir.glob(f"*_{browser}.json")) if storage_dir.exists() else [] - if not auth_files: - return None - - auth_file = auth_files[0] # 加载第一个匹配的 auth 文件 - context_kwargs["storage_state"] = str(auth_file) - # 从文件名提取角色名(格式: _.json) - role = auth_file.stem.replace(f"_{browser}", "") - return role - - -def _get_browser_name(page: Page) -> str: - """从 page 推测浏览器名(用于错误提示)。""" - return "chromium" # 默认提示 chromium - - -# ── pytest 报告 hook ── -@pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_makereport(item, call): - """记录用例执行结果,供失败截图使用。""" - outcome = yield - rep = outcome.get_result() - setattr(item, f"rep_{rep.when}", rep) diff --git a/tests/web/mini_program/conftest.py b/tests/web/mini_program/conftest.py new file mode 100644 index 0000000..1540b00 --- /dev/null +++ b/tests/web/mini_program/conftest.py @@ -0,0 +1,52 @@ +"""司机端小程序 fixture — Playwright(模拟微信 WebView)。 + +核心机制: + 模拟微信内置浏览器 UA,微信 WebView 仍需登录态。 + 首次使用 python tests/web/admin/save_auth.py 生成后用在此端也可以。 +""" + +from datetime import datetime +from pathlib import Path + +import pytest +from playwright.sync_api import sync_playwright, Page + +SCREENSHOTS_DIR = Path("output/screenshots") + + +@pytest.fixture +def page(mini_program_config, request) -> Page: + """微信 WebView 模拟 Page。""" + viewport = mini_program_config.get("viewport", {"width": 375, "height": 812}) + timeout = mini_program_config.get("timeout_seconds", 120) * 1000 + user_agent = mini_program_config.get("user_agent", "Mozilla/5.0 MicroMessenger/8.0.0") + base_url = mini_program_config["base_url"] + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + context = browser.new_context( + viewport={"width": viewport["width"], "height": viewport["height"]}, + user_agent=user_agent, + locale="zh-CN", + ) + page = context.new_page() + page.set_default_timeout(timeout) + + yield page + + if hasattr(request.node, "rep_call") and request.node.rep_call.failed: + scenario_dir = SCREENSHOTS_DIR / request.node.name + scenario_dir.mkdir(parents=True, exist_ok=True) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + path = str(scenario_dir / f"FAIL_mini_{ts}.png") + page.screenshot(path=path) + + context.close() + browser.close() + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + rep = outcome.get_result() + setattr(item, f"rep_{rep.when}", rep) diff --git a/tests/web/mini_program/pages/__init__.py b/tests/web/mini_program/pages/__init__.py new file mode 100644 index 0000000..3f3d48e --- /dev/null +++ b/tests/web/mini_program/pages/__init__.py @@ -0,0 +1,31 @@ +"""小程序 Page Object 基类。""" + +from datetime import datetime +from pathlib import Path + +from playwright.sync_api import Page, Locator, expect + +SCREENSHOTS_DIR = Path("output/screenshots") + + +class MiniProgramBasePage: + """小程序 Page Object 基类。""" + + def __init__(self, page: Page, timeout_ms: int = 30_000): + self.page = page + self.timeout = timeout_ms + + def wait_visible(self, locator: Locator, timeout_ms: int | None = None) -> None: + expect(locator).to_be_visible(timeout=timeout_ms or self.timeout) + + def click_when_ready(self, locator: Locator) -> None: + self.wait_visible(locator) + locator.click() + + def screenshot(self, name: str) -> str: + scenario_dir = SCREENSHOTS_DIR / self.__class__.__name__ + scenario_dir.mkdir(parents=True, exist_ok=True) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + path = str(scenario_dir / f"{name}_{ts}.png") + self.page.screenshot(path=path) + return path diff --git a/tests/web/mini_program/pages/mini_home_page.py b/tests/web/mini_program/pages/mini_home_page.py new file mode 100644 index 0000000..fded447 --- /dev/null +++ b/tests/web/mini_program/pages/mini_home_page.py @@ -0,0 +1,24 @@ +"""小程序首页 — Page Object。 + +Usage: + from tests.web.mini_program.pages import MiniProgramBasePage +""" + +from playwright.sync_api import Page +from tests.web.mini_program.pages import MiniProgramBasePage + + +class MiniHomePage(MiniProgramBasePage): + """司机端小程序首页。""" + + def __init__(self, page: Page): + super().__init__(page) + # ⚠️ 定位器需根据实际小程序 UI 确认 + self.order_list_tab = page.get_by_text("运单") + self.mine_tab = page.get_by_text("我的") + + def go_to_order_list(self) -> None: + self.click_when_ready(self.order_list_tab) + + def go_to_mine(self) -> None: + self.click_when_ready(self.mine_tab) diff --git a/tests/web/mini_program/tests/test_smoke.py b/tests/web/mini_program/tests/test_smoke.py new file mode 100644 index 0000000..722cdbb --- /dev/null +++ b/tests/web/mini_program/tests/test_smoke.py @@ -0,0 +1,16 @@ +"""小程序冒烟测试。""" + +import pytest + +pytestmark = [pytest.mark.mini_program] + + +class TestMiniProgramSmoke: + """小程序基础验证。""" + + @pytest.mark.p0 + @pytest.mark.smoke + def test_mini_program_accessible(self, page, mini_program_config): + """验证小程序首页可正常加载。""" + page.goto(mini_program_config["base_url"]) + assert page.title(), "小程序首页应显示页面标题" diff --git a/tests/web/test_login/test_login.py b/tests/web/test_login/test_login.py deleted file mode 100644 index a90b404..0000000 --- a/tests/web/test_login/test_login.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Web 登录功能测试用例。 - -覆盖: - - (skip_login: true) 验证预登录态有效,不经过 UI 登录页 - - (skip_login: false) 完整 UI 登录流程 + 表单校验 - -标记: - pytest -m web # 仅执行 Web 用例 - pytest -m p0 # 仅执行 P0 冒烟 - pytest -m smoke # 仅执行冒烟 -""" - -import pytest -from playwright.sync_api import Page - -pytestmark = [pytest.mark.web] - - -# ═══════════════════════════════════════════════════════════════════ -# P0 — 冒烟测试(依赖预登录态,不触发 UI 登录) -# ═══════════════════════════════════════════════════════════════════ - -class TestLoginSmoke: - """验证预登录态有效 + 后台可正常访问。""" - - @pytest.mark.p0 - @pytest.mark.smoke - def test_admin_access_with_saved_auth(self, page: Page, web_config, ensure_login): - """验证加载预登录态后可直接访问管理后台,无需重新登录。""" - ensure_login("super_admin") - page.goto(web_config["base_url"]) - # 等 hash 路由完成 - page.wait_for_timeout(2000) - # 已登录状态下不应重定向到登录页 - assert "#/login" not in page.url and "/login" not in page.url, ( - f"预登录态失效!仍然重定向到登录页。请运行: python tests/web/save_auth.py" - ) - - @pytest.mark.p0 - @pytest.mark.smoke - def test_team_leader_access_with_saved_auth(self, page: Page, web_config, ensure_login): - """验证车队长预登录态有效。""" - ensure_login("team_leader") - page.goto(web_config["base_url"]) - page.wait_for_timeout(2000) - assert "#/login" not in page.url and "/login" not in page.url - - @pytest.mark.p0 - @pytest.mark.smoke - def test_dashboard_loads(self, page: Page, web_config, ensure_login): - """验证登录后首页能正常加载。""" - ensure_login("super_admin") - page.goto(web_config["base_url"]) - # 页面标题应包含"运八"或类似标识 - assert page.title(), "首页应显示页面标题" - - -# ═══════════════════════════════════════════════════════════════════ -# P1 — 登录页交互(仅在 skip_login: false 时才有意义) -# ═══════════════════════════════════════════════════════════════════ - -class TestLoginPageDirect: - """直接访问登录页相关用例(不依赖账号)。""" - - @pytest.mark.p1 - def test_login_page_accessible(self, page: Page, web_config): - """验证登录页可直接访问并正常渲染。""" - page.goto(f"{web_config['base_url']}/#/login") - # 登录页应存在用户名输入框 - assert page.get_by_placeholder("请输入用户名").is_visible() - - @pytest.mark.p1 - def test_login_page_elements(self, page: Page, web_config): - """验证登录页所有关键元素存在。""" - page.goto(f"{web_config['base_url']}/#/login") - assert page.get_by_placeholder("请输入用户名").is_visible() - assert page.get_by_placeholder("请输入密码").is_visible() - assert page.get_by_role("button", name="登录").is_visible() - - @pytest.mark.p1 - def test_double_login_session_redirect(self, page: Page, web_config, ensure_login): - """验证已登录用户访问登录页自动重定向。""" - ensure_login("super_admin") - page.goto(web_config["base_url"]) - page.wait_for_timeout(2000) - assert "#/login" not in page.url and "/login" not in page.url - - # 已登录状态再次访问登录页 - page.goto(f"{web_config['base_url']}/#/login") - page.wait_for_timeout(2000) - # 应被重定向到后台 - assert "#/login" not in page.url and "/login" not in page.url, ( - f"已登录用户访问 /login 应被重定向,当前: {page.url}" - ) - - -# ═══════════════════════════════════════════════════════════════════ -# P2 — UI 登录流程(仅 skip_login: false 时执行,有验证码时预期跳过) -# ═══════════════════════════════════════════════════════════════════ - -@pytest.mark.p2 -@pytest.mark.skip_login_required # 自定义 marker:仅在 skip_login: false 时生效 -class TestLoginUI: - """完整 UI 登录(需要 skip_login: false)。""" - - def test_full_ui_login_super_admin(self, page: Page, web_config, ensure_login): - """通过 UI 完成完整登录(需要手动关闭 skip_login)。""" - ensure_login("super_admin") - page.goto(web_config["base_url"]) - page.wait_for_timeout(2000) - assert "#/login" not in page.url and "/login" not in page.url