From f039e94fa83aa8721e81566d75b2bb4c5c988ea4 Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Mon, 27 Apr 2026 18:20:34 +0800 Subject: [PATCH 01/14] Replace hardcoded time.sleep with explicit waits in BasePage --- UI_Automation/Pages/base_page.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index d728e69..1779462 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -8,7 +8,6 @@ - 显式等待策略 """ import os -import time from typing import List, Optional, Tuple, Union from appium import webdriver @@ -125,8 +124,6 @@ def wait_and_click( # 尝试使用 JavaScript 点击(解决 iOS 偶发点击失败问题) self.logger.debug("尝试 JS Click...") self.driver.execute_script("arguments[0].click();", element) - - time.sleep(0.3) # 短暂等待 UI 响应 return self def wait_and_input( @@ -154,13 +151,11 @@ def wait_and_input( self.logger.info(f"⌨️ {desc}") element = self._find_element(locator, timeout) - + if clear_first: element.clear() - time.sleep(0.2) - + element.send_keys(text) - time.sleep(0.3) return self def wait_and_get_text( @@ -197,7 +192,6 @@ def swipe_up(self, duration: int = 800): self.driver.swipe(start_x, start_y, start_x, end_y, duration) self.logger.debug("👆 向上滑动") - time.sleep(0.5) return self def swipe_down(self, duration: int = 800): @@ -209,7 +203,6 @@ def swipe_down(self, duration: int = 800): self.driver.swipe(start_x, start_y, start_x, end_y, duration) self.logger.debug("👇 向下滑动") - time.sleep(0.5) return self def swipe_left(self, duration: int = 500): @@ -221,7 +214,6 @@ def swipe_left(self, duration: int = 500): self.driver.swipe(start_x, y, end_x, y, duration) self.logger.debug("⬅️ 向左滑动") - time.sleep(0.5) return self def tap_by_coordinate(self, x: int, y: int): @@ -230,7 +222,6 @@ def tap_by_coordinate(self, x: int, y: int): action = TouchAction(self.driver) action.tap(x=x, y=y).perform() self.logger.debug(f"👆 坐标点击 ({x}, {y})") - time.sleep(0.3) return self # ==================== 等待方法 ==================== @@ -251,10 +242,14 @@ def wait_for_text_present( return False def wait_for_page_load(self, timeout: int = 10): - """等待页面加载完成(通过判断 activity)""" - # iOS 上可以检查当前 bundle identifier 或 page source 变化 - time.sleep(1) # 最小等待 - # 实际场景中可以根据具体 App 特征来判断 + """等待页面加载完成""" + try: + WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until( + lambda d: d.execute_script("return document.readyState") == "complete" + or len(d.find_elements("xpath", "//*")) > 0 + ) + except Exception: + pass self.logger.debug("⏳ 等待页面加载") def custom_wait(self, condition, timeout: int = DEFAULT_TIMEOUT): From e9be6b14a8989e54bd53584a77ed5ff13a6844af Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 08:50:15 +0800 Subject: [PATCH 02/14] Fix wait_for_page_load to use native-compatible explicit wait --- UI_Automation/Pages/base_page.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index 1779462..af0623f 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -242,14 +242,14 @@ def wait_for_text_present( return False def wait_for_page_load(self, timeout: int = 10): - """等待页面加载完成""" + """等待页面加载完成(等待页面元素稳定)""" + from selenium.common.exceptions import TimeoutException try: WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until( - lambda d: d.execute_script("return document.readyState") == "complete" - or len(d.find_elements("xpath", "//*")) > 0 + lambda d: len(d.find_elements("xpath", "//*")) > 0 ) - except Exception: - pass + except TimeoutException: + self.logger.warning("⏰ wait_for_page_load 超时,页面可能未完全加载") self.logger.debug("⏳ 等待页面加载") def custom_wait(self, condition, timeout: int = DEFAULT_TIMEOUT): From 3ae3d80898bf89711cc19298d8d53509f6e0c08a Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 09:01:26 +0800 Subject: [PATCH 03/14] Move TimeoutException to module-level import and improve wait_for_page_load condition --- UI_Automation/Pages/base_page.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index af0623f..cf21bb1 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -15,6 +15,7 @@ from appium.webdriver.webdriver import WebDriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException from utils.log_util import get_logger from utils.screenshot_util import ScreenshotUtil @@ -243,10 +244,9 @@ def wait_for_text_present( def wait_for_page_load(self, timeout: int = 10): """等待页面加载完成(等待页面元素稳定)""" - from selenium.common.exceptions import TimeoutException try: WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until( - lambda d: len(d.find_elements("xpath", "//*")) > 0 + EC.presence_of_element_located(("xpath", "//*[@identifier or @name]")) ) except TimeoutException: self.logger.warning("⏰ wait_for_page_load 超时,页面可能未完全加载") From 812101bf3d6e69d01a9c5e9794a495d9d9e2bb73 Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 11:00:33 +0800 Subject: [PATCH 04/14] fix: replace deprecated driver.swipe with W3C Actions and fix wait_for_page_load --- UI_Automation/Pages/base_page.py | 61 +++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index cf21bb1..10d69c8 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -187,33 +187,42 @@ def wait_and_check_visible( def swipe_up(self, duration: int = 800): """向上滑动(内容向下滚动)""" size = self.driver.get_window_size() - start_x = size["width"] / 2 - start_y = size["height"] * 0.7 - end_y = size["height"] * 0.3 - - self.driver.swipe(start_x, start_y, start_x, end_y, duration) + start_x = int(size["width"] / 2) + start_y = int(size["height"] * 0.7) + end_y = int(size["height"] * 0.3) + self.driver.execute_script("mobile: dragFromToForDuration", { + "duration": duration / 1000, + "fromX": start_x, "fromY": start_y, + "toX": start_x, "toY": end_y + }) self.logger.debug("👆 向上滑动") return self def swipe_down(self, duration: int = 800): """向下滑动(内容向上滚动)""" size = self.driver.get_window_size() - start_x = size["width"] / 2 - start_y = size["height"] * 0.3 - end_y = size["height"] * 0.7 - - self.driver.swipe(start_x, start_y, start_x, end_y, duration) + start_x = int(size["width"] / 2) + start_y = int(size["height"] * 0.3) + end_y = int(size["height"] * 0.7) + self.driver.execute_script("mobile: dragFromToForDuration", { + "duration": duration / 1000, + "fromX": start_x, "fromY": start_y, + "toX": start_x, "toY": end_y + }) self.logger.debug("👇 向下滑动") return self def swipe_left(self, duration: int = 500): """向左滑动""" size = self.driver.get_window_size() - start_x = size["width"] * 0.8 - end_x = size["width"] * 0.2 - y = size["height"] / 2 - - self.driver.swipe(start_x, y, end_x, y, duration) + start_x = int(size["width"] * 0.8) + end_x = int(size["width"] * 0.2) + y = int(size["height"] / 2) + self.driver.execute_script("mobile: dragFromToForDuration", { + "duration": duration / 1000, + "fromX": start_x, "fromY": y, + "toX": end_x, "toY": y + }) self.logger.debug("⬅️ 向左滑动") return self @@ -242,15 +251,25 @@ def wait_for_text_present( self.logger.warning(f"⏰ 文本未出现: {text}") return False - def wait_for_page_load(self, timeout: int = 10): - """等待页面加载完成(等待页面元素稳定)""" + def wait_for_page_load(self, condition=None, timeout: int = 10) -> bool: + """等待页面加载完成 + + Args: + condition: 自定义等待条件(lambda driver: bool),默认等待元素数量 > 1 + timeout: 超时时间 + + Returns: + True 表示加载成功,False 表示超时 + """ + self.logger.debug("⏳ 等待页面加载") + wait_condition = condition or (lambda d: len(d.find_elements("xpath", "//*")) > 1) try: - WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until( - EC.presence_of_element_located(("xpath", "//*[@identifier or @name]")) - ) + WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until(wait_condition) + self.logger.debug("✅ 页面加载完成") + return True except TimeoutException: self.logger.warning("⏰ wait_for_page_load 超时,页面可能未完全加载") - self.logger.debug("⏳ 等待页面加载") + return False def custom_wait(self, condition, timeout: int = DEFAULT_TIMEOUT): """自定义等待条件""" From 15009def95748b5ef38e97b14140cf984c9ffd9f Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 11:29:02 +0800 Subject: [PATCH 05/14] refactor: extract _drag helper, rename duration to duration_ms, lighten wait_for_page_load default --- UI_Automation/Pages/base_page.py | 54 +++++++++++++------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index 10d69c8..3ec5607 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -183,46 +183,36 @@ def wait_and_check_visible( return False # ==================== 手势操作 ==================== - - def swipe_up(self, duration: int = 800): - """向上滑动(内容向下滚动)""" - size = self.driver.get_window_size() - start_x = int(size["width"] / 2) - start_y = int(size["height"] * 0.7) - end_y = int(size["height"] * 0.3) + + def _drag(self, from_x: int, from_y: int, to_x: int, to_y: int, duration_ms: int): + """执行 W3C drag 手势(内部 helper)""" self.driver.execute_script("mobile: dragFromToForDuration", { - "duration": duration / 1000, - "fromX": start_x, "fromY": start_y, - "toX": start_x, "toY": end_y + "duration": duration_ms / 1000, + "fromX": from_x, "fromY": from_y, + "toX": to_x, "toY": to_y }) + + def swipe_up(self, duration_ms: int = 800): + """向上滑动(内容向下滚动)。duration_ms 单位为毫秒""" + size = self.driver.get_window_size() + x = int(size["width"] / 2) + self._drag(x, int(size["height"] * 0.7), x, int(size["height"] * 0.3), duration_ms) self.logger.debug("👆 向上滑动") return self - - def swipe_down(self, duration: int = 800): - """向下滑动(内容向上滚动)""" + + def swipe_down(self, duration_ms: int = 800): + """向下滑动(内容向上滚动)。duration_ms 单位为毫秒""" size = self.driver.get_window_size() - start_x = int(size["width"] / 2) - start_y = int(size["height"] * 0.3) - end_y = int(size["height"] * 0.7) - self.driver.execute_script("mobile: dragFromToForDuration", { - "duration": duration / 1000, - "fromX": start_x, "fromY": start_y, - "toX": start_x, "toY": end_y - }) + x = int(size["width"] / 2) + self._drag(x, int(size["height"] * 0.3), x, int(size["height"] * 0.7), duration_ms) self.logger.debug("👇 向下滑动") return self - - def swipe_left(self, duration: int = 500): - """向左滑动""" + + def swipe_left(self, duration_ms: int = 500): + """向左滑动。duration_ms 单位为毫秒""" size = self.driver.get_window_size() - start_x = int(size["width"] * 0.8) - end_x = int(size["width"] * 0.2) y = int(size["height"] / 2) - self.driver.execute_script("mobile: dragFromToForDuration", { - "duration": duration / 1000, - "fromX": start_x, "fromY": y, - "toX": end_x, "toY": y - }) + self._drag(int(size["width"] * 0.8), y, int(size["width"] * 0.2), y, duration_ms) self.logger.debug("⬅️ 向左滑动") return self @@ -262,7 +252,7 @@ def wait_for_page_load(self, condition=None, timeout: int = 10) -> bool: True 表示加载成功,False 表示超时 """ self.logger.debug("⏳ 等待页面加载") - wait_condition = condition or (lambda d: len(d.find_elements("xpath", "//*")) > 1) + wait_condition = condition or (lambda d: len(d.page_source) > 100) try: WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until(wait_condition) self.logger.debug("✅ 页面加载完成") From 1695dbbfb14265f774a5f2a13475d8235f794bd0 Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 13:48:38 +0800 Subject: [PATCH 06/14] docs: clarify _drag iOS-only constraint and improve wait_for_page_load docstring --- UI_Automation/Pages/base_page.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index 3ec5607..15f3fd0 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -185,7 +185,11 @@ def wait_and_check_visible( # ==================== 手势操作 ==================== def _drag(self, from_x: int, from_y: int, to_x: int, to_y: int, duration_ms: int): - """执行 W3C drag 手势(内部 helper)""" + """执行 W3C drag 手势(内部 helper) + + 注意:仅支持 iOS XCUITest driver,使用 mobile: dragFromToForDuration 命令。 + duration_ms 单位为毫秒,内部转换为秒传给 Appium。 + """ self.driver.execute_script("mobile: dragFromToForDuration", { "duration": duration_ms / 1000, "fromX": from_x, "fromY": from_y, @@ -245,11 +249,18 @@ def wait_for_page_load(self, condition=None, timeout: int = 10) -> bool: """等待页面加载完成 Args: - condition: 自定义等待条件(lambda driver: bool),默认等待元素数量 > 1 - timeout: 超时时间 + condition: 自定义等待条件(lambda driver: bool)。 + 强烈建议调用方传入页面特定的条件(如等待某个关键元素出现), + 默认条件仅检查 page_source 长度,可能在某些页面产生误判。 + timeout: 超时时间(秒) Returns: True 表示加载成功,False 表示超时 + + Example: + page.wait_for_page_load( + condition=lambda d: d.find_elements(AppiumBy.ACCESSIBILITY_ID, "home_title") + ) """ self.logger.debug("⏳ 等待页面加载") wait_condition = condition or (lambda d: len(d.page_source) > 100) From 84ab8d4ecdfed613c1fd85ffbe91b147b20e778f Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 13:53:46 +0800 Subject: [PATCH 07/14] fix: add platform guard in _drag and warn when wait_for_page_load has no condition --- UI_Automation/Pages/base_page.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index 15f3fd0..39eff64 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -190,6 +190,11 @@ def _drag(self, from_x: int, from_y: int, to_x: int, to_y: int, duration_ms: int 注意:仅支持 iOS XCUITest driver,使用 mobile: dragFromToForDuration 命令。 duration_ms 单位为毫秒,内部转换为秒传给 Appium。 """ + platform = self.driver.capabilities.get("platformName", "").lower() + if platform != "ios": + raise RuntimeError( + f"_drag 仅支持 iOS XCUITest driver,当前平台: {platform}" + ) self.driver.execute_script("mobile: dragFromToForDuration", { "duration": duration_ms / 1000, "fromX": from_x, "fromY": from_y, @@ -263,6 +268,8 @@ def wait_for_page_load(self, condition=None, timeout: int = 10) -> bool: ) """ self.logger.debug("⏳ 等待页面加载") + if condition is None: + self.logger.warning("⚠️ wait_for_page_load 未传入 condition,使用通用默认条件,建议传入页面特定的等待条件") wait_condition = condition or (lambda d: len(d.page_source) > 100) try: WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until(wait_condition) From d9f015d14614e0b0829f6df49244089a82ddbe1f Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 14:21:00 +0800 Subject: [PATCH 08/14] refactor: declare iOS-only scope, remove platform guard, make wait_for_page_load condition required --- UI_Automation/Pages/base_page.py | 42 ++++++++++++-------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/UI_Automation/Pages/base_page.py b/UI_Automation/Pages/base_page.py index 39eff64..5e1487f 100644 --- a/UI_Automation/Pages/base_page.py +++ b/UI_Automation/Pages/base_page.py @@ -1,6 +1,9 @@ """ BasePage - 页面基类(Page Object 模式的核心) +本框架专为 iOS + XCUITest 设计,不支持 Android 或其他平台。 +所有手势操作均基于 Appium mobile: 扩展命令。 + 封装了所有通用的页面操作: - 元素定位与等待 - 点击、输入、滑动 @@ -23,14 +26,15 @@ class BasePage: """ - Page Object 基类 - + Page Object 基类(仅支持 iOS + XCUITest) + 所有页面的父类,提供: 1. 统一的元素操作方法(wait_and_click, wait_and_input 等) - 2. 多种元素定位策略自动切换 - 3. 显式等待机制 - 4. 自动失败截图 - 5. 操作日志记录 + 2. 显式等待机制 + 3. 自动失败截图 + 4. 操作日志记录 + + 注意:手势操作(swipe_*)依赖 Appium mobile: 扩展命令,仅适用于 iOS XCUITest driver。 """ # 默认超时时间(秒) @@ -185,16 +189,11 @@ def wait_and_check_visible( # ==================== 手势操作 ==================== def _drag(self, from_x: int, from_y: int, to_x: int, to_y: int, duration_ms: int): - """执行 W3C drag 手势(内部 helper) + """执行 drag 手势(内部 helper) - 注意:仅支持 iOS XCUITest driver,使用 mobile: dragFromToForDuration 命令。 + 使用 Appium mobile: dragFromToForDuration 命令。 duration_ms 单位为毫秒,内部转换为秒传给 Appium。 """ - platform = self.driver.capabilities.get("platformName", "").lower() - if platform != "ios": - raise RuntimeError( - f"_drag 仅支持 iOS XCUITest driver,当前平台: {platform}" - ) self.driver.execute_script("mobile: dragFromToForDuration", { "duration": duration_ms / 1000, "fromX": from_x, "fromY": from_y, @@ -250,29 +249,20 @@ def wait_for_text_present( self.logger.warning(f"⏰ 文本未出现: {text}") return False - def wait_for_page_load(self, condition=None, timeout: int = 10) -> bool: + def wait_for_page_load(self, condition, timeout: int = 10) -> bool: """等待页面加载完成 Args: - condition: 自定义等待条件(lambda driver: bool)。 - 强烈建议调用方传入页面特定的条件(如等待某个关键元素出现), - 默认条件仅检查 page_source 长度,可能在某些页面产生误判。 + condition: 等待条件(lambda driver: bool),必须传入页面特定的条件。 + 例如:lambda d: d.find_elements(AppiumBy.ACCESSIBILITY_ID, "home_title") timeout: 超时时间(秒) Returns: True 表示加载成功,False 表示超时 - - Example: - page.wait_for_page_load( - condition=lambda d: d.find_elements(AppiumBy.ACCESSIBILITY_ID, "home_title") - ) """ self.logger.debug("⏳ 等待页面加载") - if condition is None: - self.logger.warning("⚠️ wait_for_page_load 未传入 condition,使用通用默认条件,建议传入页面特定的等待条件") - wait_condition = condition or (lambda d: len(d.page_source) > 100) try: - WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until(wait_condition) + WebDriverWait(self.driver, timeout, self.POLL_FREQUENCY).until(condition) self.logger.debug("✅ 页面加载完成") return True except TimeoutException: From c8bf9f5ad3f81bb70fc110dd9d7ca9674dd5765d Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 14:48:34 +0800 Subject: [PATCH 09/14] fix: correct allure decorator typo (allue -> allure) in API test cases --- API_Automation/cases/test_cart.py | 6 +++--- API_Automation/cases/test_order.py | 10 +++++----- API_Automation/cases/test_product.py | 20 ++++++++++---------- API_Automation/cases/test_user.py | 10 +++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/API_Automation/cases/test_cart.py b/API_Automation/cases/test_cart.py index c302a7c..cc0e56c 100644 --- a/API_Automation/cases/test_cart.py +++ b/API_Automation/cases/test_cart.py @@ -7,7 +7,7 @@ @allure.feature("购物车模块") -@allue.story("购物车操作") +@allure.story("购物车操作") class TestCart: """购物车接口测试""" @@ -17,7 +17,7 @@ def setup(self, request_util, login_token): self.product_api = ProductAPI(request_util) self.token = login_token - @allue.title("添加商品到购物车-正常流程") + @allure.title("添加商品到购物车-正常流程") @allure.severity(allure.severity_level.CRITICAL) @pytest.mark.smoke def test_add_to_cart(self): @@ -37,7 +37,7 @@ def test_add_to_cart(self): items = cart.get("data", {}).get("items", []) assert len(items) > 0, "购物车应为空" - @allue.title("获取购物车汇总信息") + @allure.title("获取购物车汇总信息") @allure.severity(allure.severity_level.NORMAL) def test_cart_summary(self): # 先确保有商品 diff --git a/API_Automation/cases/test_order.py b/API_Automation/cases/test_order.py index d76e875..96a38a1 100644 --- a/API_Automation/cases/test_order.py +++ b/API_Automation/cases/test_order.py @@ -6,7 +6,7 @@ @allure.feature("订单模块") -@allue.story("订单管理") +@allure.story("订单管理") class TestOrder: """订单接口测试""" @@ -15,8 +15,8 @@ def setup(self, request_util, login_token): self.order_api = OrderAPI(request_util) self.token = login_token - @allue.title("获取订单列表") - @allue.severity(allure.severity_level.CRITICAL) + @allure.title("获取订单列表") + @allure.severity(allure.severity_level.CRITICAL) @pytest.mark.smoke def test_get_order_list(self): with allure.step("获取全部订单"): @@ -31,8 +31,8 @@ def test_get_order_list(self): attachment_type=allure.attachment_type.TEXT ) - @allue.title("按状态筛选订单") - @allue.severity(allure.severity_level.NORMAL) + @allure.title("按状态筛选订单") + @allure.severity(allure.severity_level.NORMAL) @pytest.mark.parametrize("status", ["pending", "shipping", "completed", "cancelled"]) def test_filter_order_by_status(self, status): with allure.step(f"筛选状态: {status}"): diff --git a/API_Automation/cases/test_product.py b/API_Automation/cases/test_product.py index f95ef91..337ceb3 100644 --- a/API_Automation/cases/test_product.py +++ b/API_Automation/cases/test_product.py @@ -6,7 +6,7 @@ @allure.feature("商品模块") -@allue.story("商品搜索") +@allure.story("商品搜索") class TestProductSearch: """商品搜索接口测试""" @@ -15,8 +15,8 @@ def setup(self, request_util): self.product_api = ProductAPI(request_util) self.assertion = AssertUtil() - @allue.title("搜索关键词'手机'- 应有结果") - @allue.severity(allure.severity_level.CRITICAL) + @allure.title("搜索关键词'手机'- 应有结果") + @allure.severity(allure.severity_level.CRITICAL) @pytest.mark.smoke def test_search_phone(self): with allure.step("搜索'手机'"): @@ -28,8 +28,8 @@ def test_search_phone(self): with allure.step(f"结果数量: {len(products)}"): assert len(products) > 0, "搜索无结果" - @allue.title("搜索不存在的商品- 返回空列表") - @allue.severity(allure.severity_level.NORMAL) + @allure.title("搜索不存在的商品- 返回空列表") + @allure.severity(allure.severity_level.NORMAL) @pytest.mark.regression def test_search_not_exist(self): with allure.step("搜索不存在的关键词"): @@ -43,7 +43,7 @@ def test_search_not_exist(self): @allure.feature("商品模块") -@allue.story("分类浏览") +@allure.story("分类浏览") class TestCategory: """分类接口测试""" @@ -51,8 +51,8 @@ class TestCategory: def setup(self, request_util): self.product_api = ProductAPI(request_util) - @allue.title("获取一级分类列表") - @allue.severity(allure.severity_level.CRITICAL) + @allure.title("获取一级分类列表") + @allure.severity(allure.severity_level.CRITICAL) @pytest.mark.smoke def test_get_category_list(self): with allure.step("获取一级分类"): @@ -64,8 +64,8 @@ def test_get_category_list(self): with allure.step(f"分类数量 >= 5"): assert len(categories) >= 5, f"一级分类数量过少: {len(categories)}" - @allue.title("获取子分类") - @allue.severity(allure.severity_level.NORMAL) + @allure.title("获取子分类") + @allure.severity(allure.severity_level.NORMAL) def test_get_sub_category(self): with allure.step("获取数码类子分类"): result = self.product_api.get_category_list(parent_id=1) diff --git a/API_Automation/cases/test_user.py b/API_Automation/cases/test_user.py index ac90c33..2051491 100644 --- a/API_Automation/cases/test_user.py +++ b/API_Automation/cases/test_user.py @@ -124,7 +124,7 @@ def test_login_invalid_phone_format( @allure.feature("用户模块") -@allue.story("用户信息") +@allure.story("用户信息") class TestUserInfo: """用户信息接口测试""" @@ -133,8 +133,8 @@ def setup(self, request_util, login_token): self.user_api = UserAPI(request_util) self.token = login_token - @allue.title("获取当前用户信息") - @allue.severity(allure.severity_level.CRITICAL) + @allure.title("获取当前用户信息") + @allure.severity(allure.severity_level.CRITICAL) @pytest.mark.smoke def test_get_user_info(self): with allure.step("获取用户详情"): @@ -153,8 +153,8 @@ def test_get_user_info(self): attachment_type=allure.attachment_type.JSON ) - @allue.title("修改昵称") - @allue.severity(allure.severity_level.NORMAL) + @allure.title("修改昵称") + @allure.severity(allure.severity_level.NORMAL) def test_update_nickname(self): new_name = f"AutoTest_{__import__('time').strftime('%H%M%S')}" From d681dc98a0c03a1673f7033cc9b3d58a49db61c5 Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 15:01:01 +0800 Subject: [PATCH 10/14] fix: correct invalid return type annotation in get_logger causing NameError --- utils/log_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/log_util.py b/utils/log_util.py index ed793b4..d983400 100644 --- a/utils/log_util.py +++ b/utils/log_util.py @@ -96,7 +96,7 @@ def get_logger(cls, name: str = None) -> logger: # 导出便捷函数 -def get_logger(name: str = None) -> LogUtil.get_logger(name): +def get_logger(name: str = None) -> "LogUtil": """获取 Logger 的快捷方式""" return LogUtil.get_logger(name) From d207d292fd404026f003907c9aad43ef06bd4f5f Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 15:06:23 +0800 Subject: [PATCH 11/14] fix: add default None to token params in order_api and fix assertion message --- API_Automation/api/order_api.py | 12 ++++++------ API_Automation/cases/test_cart.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/API_Automation/api/order_api.py b/API_Automation/api/order_api.py index c4c7bac..7da1d67 100644 --- a/API_Automation/api/order_api.py +++ b/API_Automation/api/order_api.py @@ -68,10 +68,10 @@ def get_order_count(self, status: str = "all", token: str = None) -> dict: # ===== 订单操作 ===== def cancel_order( - self, - order_no: str, + self, + order_no: str, reason: str = "", - token: str + token: str = None ) -> dict: """取消订单""" return self.post( @@ -91,10 +91,10 @@ def confirm_receive( # ===== 支付 ===== def get_pay_params( - self, - order_no: str, + self, + order_no: str, pay_method: str = "wechat", # wechat/alipay - token: str + token: str = None ) -> dict: """获取支付参数""" return self.post( diff --git a/API_Automation/cases/test_cart.py b/API_Automation/cases/test_cart.py index cc0e56c..a28a246 100644 --- a/API_Automation/cases/test_cart.py +++ b/API_Automation/cases/test_cart.py @@ -35,7 +35,7 @@ def test_add_to_cart(self): cart = self.cart_api.get_cart_list(self.token) AssertUtil().assert_response_code(cart) items = cart.get("data", {}).get("items", []) - assert len(items) > 0, "购物车应为空" + assert len(items) > 0, "购物车不应为空" @allure.title("获取购物车汇总信息") @allure.severity(allure.severity_level.NORMAL) From d22b322624386273998f5ce0da2e8a41caa46adc Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 15:14:25 +0800 Subject: [PATCH 12/14] fix: skip integration tests when API_BASE_URL unset, allow test-reporter to fail on PR --- .github/workflows/ci.yml | 1 + conftest.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87cdbf6..d3dffe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: - name: 发布 JUnit 结果 uses: dorny/test-reporter@v1 if: always() + continue-on-error: true with: name: API Test Results path: Reports/junit-api.xml diff --git a/conftest.py b/conftest.py index b757bfb..59ffb63 100644 --- a/conftest.py +++ b/conftest.py @@ -107,6 +107,16 @@ def pytest_configure(config): config.addinivalue_line("markers", "ui: UI测试") +def pytest_collection_modifyitems(items): + """无 API_BASE_URL 时跳过所有接口集成测试""" + if os.getenv("API_BASE_URL"): + return + skip = pytest.mark.skip(reason="API_BASE_URL not set; skipping integration tests") + for item in items: + if "API_Automation/cases" in str(item.fspath).replace("\\", "/"): + item.add_marker(skip) + + @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """ From 92f685c58b67e413a60508f98d231dcbb0e49ab3 Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 27 Apr 2026 20:21:23 +0800 Subject: [PATCH 13/14] fix: add checks:write permission to api-test job --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3dffe7..a52f555 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ on: jobs: api-test: runs-on: ubuntu-latest + permissions: + checks: write steps: - uses: actions/checkout@v4 From 018c5906c8c9912777c70412098d8d631e03e61d Mon Sep 17 00:00:00 2001 From: ConnorQi01 Date: Tue, 28 Apr 2026 16:19:34 +0800 Subject: [PATCH 14/14] feat: enable parallel execution for API tests via pytest-xdist --- pytest.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index be3ee1c..e3f4a0c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -24,8 +24,9 @@ allure_severities = blocker critical normal minor trivial allure_epics = 用户模块 商品模块 购物车模块 订单模块 allure_features = 登录 注册 首页 分类 详情 购物车 下单 支付 -# 重试 -addopts = -v --tb=short --strict-markers --reruns 2 --reruns-delay 1 +# 重试和并发 +# API 测试并发执行,UI 测试串行(设备独占) +addopts = -v --tb=short --strict-markers --reruns 2 --reruns-delay 1 -n auto # 超时(需要 pip install pytest-timeout) timeout = 300