1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-22 01:13:02 +08:00

refactor(pages): 拆分 _dialogs 为独立组件文件,解耦 Service 构造函数,消除 PageObject 重复逻辑

- 将 _dialogs.py 拆分为 pages/components/ 下的独立文件,Overlay 基类同步移入
- CaptchaHandler / RecordChecker 构造函数不再持有 PageObject,改为方法参数注入
- LoginPage.login() 直接接收 auto_captcha 参数,简化 captcha_solver 调用链
- SeatMapOverlay.selectSeat 引入两层查找:先按 ID 直查,失败后遍历匹配
- 移除 ReserveView 中与 Dialog/Overlay 重复的方法(selectSeat、getAvailableTimeOptions)
- AutoLibPages 拆分 __initPagesServices / __initPagesFlows
- 修复 RecordsView.MORE_BTN 选择器被错误 snake_case 化(more_btn → moreBtn)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 13:41:55 +08:00
parent 2226e8ac90
commit a6bc103c73
19 changed files with 608 additions and 580 deletions
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
StaleElementReferenceException,
TimeoutException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from pages.components.Overlay import Overlay
class CheckinResultDialog(Overlay):
"""
Check-in result dialog.
"""
ROOT = (By.CLASS_NAME, "ui_dialog")
RESULT_MSG = (By.CLASS_NAME, "resultMessage")
OK_BTN = (By.CLASS_NAME, "btnOK")
DETAIL_DD = (By.CSS_SELECTOR, ".resultMessage dd")
def __init__(
self,
driver: WebDriver,
) -> None:
super().__init__(driver, self.ROOT, auto_close_on_exit=False)
def getResultMessage(
self,
) -> str:
try:
self._waitPresence(self.RESULT_MSG)
el = self._find(*self.RESULT_MSG)
return el.text
except (TimeoutException, NoSuchElementException, StaleElementReferenceException):
return ""
except Exception:
return ""
def getDetails(
self,
) -> list[str]:
try:
elements = self._findAll(*self.DETAIL_DD)
return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException):
return []
except Exception:
return []
def clickOk(
self,
) -> bool:
try:
self._waitClickable(self.OK_BTN).click()
return True
except (NoSuchElementException, TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False
+110
View File
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class Overlay:
"""
Context-managed overlay / modal / dialog on a page.
Automates the lifecycle: wait for appearance on enter,
optionally wait for disappearance on exit.
"""
def __init__(
self,
driver: WebDriver,
root_locator: tuple,
auto_close_on_exit: bool = True,
wait_timeout: float = 3.0,
) -> None:
self._driver: WebDriver = driver
self._root_locator: tuple = root_locator
self._auto_close: bool = auto_close_on_exit
self._timeout: float = wait_timeout
def __enter__(
self,
) -> "Overlay":
WebDriverWait(self._driver, self._timeout).until(
EC.visibility_of_element_located(self._root_locator)
)
return self
def __exit__(
self,
*args: object,
) -> None:
if self._auto_close:
WebDriverWait(self._driver, self._timeout).until(
EC.invisibility_of_element_located(self._root_locator)
)
def _find(
self,
by: str,
value: str,
) -> WebElement:
return self._driver.find_element(by, value)
def _findAll(
self,
by: str,
value: str,
) -> list[WebElement]:
return self._driver.find_elements(by, value)
def _waitClickable(
self,
locator: tuple,
timeout: float = 2.0,
) -> WebElement:
return WebDriverWait(self._driver, timeout).until(
EC.element_to_be_clickable(locator)
)
def _waitPresence(
self,
locator: tuple,
timeout: float = 2.0,
) -> WebElement:
return WebDriverWait(self._driver, timeout).until(
EC.presence_of_element_located(locator)
)
def _waitVisible(
self,
locator: tuple,
timeout: float = 2.0,
) -> WebElement:
return WebDriverWait(self._driver, timeout).until(
EC.visibility_of_element_located(locator)
)
def _waitAllPresence(
self,
locator: tuple,
timeout: float = 2.0,
) -> list[WebElement]:
return WebDriverWait(self._driver, timeout).until(
EC.presence_of_all_elements_located(locator)
)
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
TimeoutException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from pages.components.Overlay import Overlay
class RenewDialog(Overlay):
"""
Renewal time selection dialog.
"""
ROOT = (By.ID, "extendDiv")
MESSAGE_HEAD = (By.CSS_SELECTOR, "#extendDiv p.messageHead")
RESULT_MSG = (By.CSS_SELECTOR, "#extendDiv div.resultMessage")
TIME_OPTS = (By.CSS_SELECTOR, "#extendDiv .renewal_List li")
OK_BTN = (By.CSS_SELECTOR, "#extendDiv .btnOK")
def __init__(
self,
driver: WebDriver,
) -> None:
super().__init__(driver, self.ROOT, auto_close_on_exit=False)
def waitUntilReady(
self,
) -> bool:
try:
self._waitVisible(self.ROOT)
self._waitPresence(self.MESSAGE_HEAD)
self._waitPresence(self.RESULT_MSG)
except (NoSuchElementException, TimeoutException):
return False
except Exception:
return False
head_msg = self._find(*self.MESSAGE_HEAD).text.strip()
if "警告" in head_msg:
return False
try:
self._waitAllPresence(self.TIME_OPTS)
self._waitPresence(self.OK_BTN)
except (NoSuchElementException, TimeoutException):
return False
except Exception:
return False
return True
def getHeadMessage(
self,
) -> str:
return self._find(*self.MESSAGE_HEAD).text.strip()
def getResultMessage(
self,
) -> str:
return self._find(*self.RESULT_MSG).text.strip()
def getTimeOptions(
self,
) -> list[WebElement]:
return self._findAll(*self.TIME_OPTS)
def getOkButton(
self,
) -> WebElement:
return self._find(*self.OK_BTN)
def clickOk(
self,
) -> bool:
try:
self._find(*self.OK_BTN).click()
return True
except (NoSuchElementException, TimeoutException, ElementNotInteractableException):
return False
except Exception:
return False
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from selenium.common.exceptions import (
NoSuchElementException,
StaleElementReferenceException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from pages.components.Overlay import Overlay
class ReserveResultDialog(Overlay):
"""
Reservation result dialog shown after submitting a reserve request.
"""
ROOT = (By.CLASS_NAME, "layoutSeat")
def __init__(
self,
driver: WebDriver,
) -> None:
super().__init__(driver, self.ROOT, auto_close_on_exit=False)
def getTitle(
self,
) -> str:
try:
return self._find(*self._title_locator()).text
except (NoSuchElementException, StaleElementReferenceException):
return ""
except Exception:
return ""
def isSuccess(
self,
) -> bool:
title = self.getTitle()
return any(
kw in title
for kw in ("预定好了", "预约成功", "操作成功")
)
def isFailure(
self,
) -> bool:
contents = self.getDetailTexts()
return any(
"预约失败" in msg or "已有1个有效预约" in msg
for msg in contents
)
def getDetailTexts(
self,
) -> list[str]:
try:
elements = self._findAll(By.CSS_SELECTOR, ".layoutSeat dd")
return [el.text for el in elements if el.text.strip()]
except (NoSuchElementException, StaleElementReferenceException):
return []
except Exception:
return []
def _title_locator(
self,
) -> tuple:
return (By.CSS_SELECTOR, ".layoutSeat dt")
+74
View File
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from selenium.common.exceptions import (
ElementNotInteractableException,
NoSuchElementException,
StaleElementReferenceException,
TimeoutException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from pages.components.Overlay import Overlay
class SeatMapOverlay(Overlay):
"""
Seat selection overlay that opens after choosing a floor and room.
"""
ROOT = (By.ID, "seatLayout")
SEAT_ITEMS = (By.CSS_SELECTOR, "li[id^='seat_']")
def __init__(
self,
driver: WebDriver,
) -> None:
super().__init__(driver, self.ROOT)
def selectSeat(
self,
seat_id: str,
) -> str | None:
try:
self._waitAllPresence(self.SEAT_ITEMS)
except (NoSuchElementException, TimeoutException):
return None
except Exception:
return None
try:
seat_el = self._find(By.ID, f"seat_{int(seat_id):03d}")
seat_link = seat_el.find_element(By.TAG_NAME, "a")
self._waitClickable((By.TAG_NAME, "a"))
seat_link.click()
return seat_link.get_attribute("title")
except (NoSuchElementException, ValueError, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException):
pass
except Exception:
pass
try:
all_seats = self._findAll(*self.SEAT_ITEMS)
seat_id_upper = seat_id.lstrip('0').upper()
for seat in all_seats:
if not seat_id_upper == seat.text.lstrip('0'):
continue
seat_link = seat.find_element(By.TAG_NAME, "a")
self._waitClickable((By.TAG_NAME, "a"))
seat_link.click()
return seat_link.get_attribute("title")
return None
except (NoSuchElementException, TimeoutException,
ElementNotInteractableException, StaleElementReferenceException):
return None
except Exception:
return None
+54
View File
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from pages.components.Overlay import Overlay
class TimeSelectDialog(Overlay):
"""
Time selection panel that appears after selecting a seat.
Contains start-time and end-time option lists.
Does NOT auto-close — the reserve submission handles cleanup.
"""
ROOT = (By.CSS_SELECTOR, "#startTime ul")
def __init__(
self,
driver: WebDriver,
) -> None:
super().__init__(driver, self.ROOT, auto_close_on_exit=False)
def getTimeOptions(
self,
time_id: str,
) -> list[WebElement]:
try:
self._waitAllPresence(
(By.CSS_SELECTOR, f"#{time_id} ul li a")
)
except (NoSuchElementException, TimeoutException):
return []
except Exception:
return []
return self._findAll(
By.CSS_SELECTOR,
f"#{time_id} ul li a",
)
+22
View File
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from pages.components.SeatMapOverlay import SeatMapOverlay
from pages.components.TimeSelectDialog import TimeSelectDialog
from pages.components.ReserveResultDialog import ReserveResultDialog
from pages.components.CheckinResultDialog import CheckinResultDialog
from pages.components.RenewDialog import RenewDialog
__all__ = [
"SeatMapOverlay",
"TimeSelectDialog",
"ReserveResultDialog",
"CheckinResultDialog",
"RenewDialog",
]