练习题-携程

https://www.ctrip.com/

测试需求

- 需求1(R001): 用户进入机票查询页面,点击【单程】按钮进入单程机票查询页面,输入国内非港澳台出发地、目的地,在乘客类型中勾选【带儿童】复选框,在舱型下拉框中选择【经济舱】选项,选择恰当出发日期点击搜索进行查询后,系统应能正确显示符合条件的航班列表,截图。(用例数不超过4条)

    输入数据要求:

- 出发地:仅考虑以下2个城市(北京、上海);

- 目的地:仅考虑以下2个城市(广州、成都);

- 出发日:2025-09-11;

- 需求2(R002): 用户进入机票查询页面,点击【单程】按钮进入单程机票查询页面,用户输入非法出发地后,系统应显示相应的错误提示信息“对不起,暂不支持该地点”,截图。(用例数不超过2条)

        输入数据要求:

- 出发地:输入任意能弹出题干所给错误提示信息的非法出发地;

- 需求3(R003): 用户进入机票查询页面,点击【单程】按钮进入单程机票查询页面,输入国内非港澳台出发地、国际及中国港澳台目的地,操作乘客类型下拉框,选择乘客类型为2成人、1儿童、1婴儿,选择恰当出发日期点击搜索进行查询后,系统应能正确显示符合条件的航班列表,截图。(用例数不超过4条)

    输入数据要求:

- 出发地:仅考虑以下2个城市(北京、上海);

- 目的地:仅考虑以下2个城市(东京、大阪);

- 出发日:2025-09-11;

代码:

import os
from datetime import datetime
import pytest
from selenium import webdriver
from selenium.webdriver import ActionChains, Keys
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from time import sleep
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

@pytest.fixture(scope="function")
def driver():
    service = Service(
        # 提交最终代码脚本时,请将驱动路径换回官方路径"C:\\Users\\86153\\AppData\\Local\\Google\\Chrome\\Application\\chromedriver.exe"
        executable_path="C:\\Users\\86153\\AppData\\Local\Google\\Chrome\\Application\\chromedriver.exe")
    driver = webdriver.Chrome(service=service)
    driver.get("https://www.ctrip.com/ ")
    driver.maximize_window()
    yield driver
    driver.quit()


class TestCtripFlight:

    # test-code-start

    def test_CtripFlight_R001(self, driver):
        """需求1:国内出发地+目的地,带儿童,经济舱,合法查询"""
        test_cases = [
            ("北京", "广州"),
            ("北京", "成都"),
            ("上海", "广州"),
            ("上海", "成都")
        ]
        for i, (dep, arr) in enumerate(test_cases, 1):
            # 重新进入首页,清除状态
            driver.get("https://www.ctrip.com")
            sleep(1)

            # 悬停到【机票】
            flight_menu = driver.find_element(By.CSS_SELECTOR, 'button[aria-label="机票 按回车键打开菜单"]')
            ActionChains(driver).move_to_element(flight_menu).perform()
            sleep(0.5)

            # 点击【国内/国际/中国港澳台】
            driver.find_element(By.LINK_TEXT, "国内/国际/中国港澳台").click()
            sleep(1)

            # 确保单程选中(默认通常已选)
            one_way_label = driver.find_element(By.XPATH,
                                                '//span[contains(@class, "radio-label") and contains(text(), "单程")]')
            if "iconf-radio-unselect" in one_way_label.find_element(By.TAG_NAME, "i").get_attribute("class"):
                one_way_label.click()
            sleep(0.5)

            # 输入出发地
            dep_input = driver.find_element(By.NAME, "owDCity")
            dep_input.clear()
            dep_input.click()
            sleep(0.2)
            dep_input.send_keys(dep)
            sleep(0.5)
            dep_input.send_keys(Keys.ENTER)

            # 输入目的地
            arr_input = driver.find_element(By.NAME, "owACity")
            arr_input.clear()
            arr_input.click()
            sleep(0.2)
            arr_input.send_keys(arr)
            sleep(0.5)
            arr_input.send_keys(Keys.ENTER)

            # 点击日期输入框
            date_input = driver.find_element(By.XPATH, '//input[@aria-label="请选择日期"]')
            date_input.click()
            sleep(0.5)  # 等待日历弹出
            # 等待日历面板出现(通过“今天”按钮判断)
            calendar_panel = WebDriverWait(driver, 10).until(
                EC.presence_of_element_located(
                    (By.XPATH, '//div[contains(@class, "calendar") or contains(@class, "date-picker")]'))
            )
            # 再定位“11.12”日
            target_day = calendar_panel.find_element(By.XPATH,
                                                     '//div[contains(@class, "date-picker")]//div[contains(@class, "date-day") and .//span[@class="date-d" and text()="12"]]')
            target_day.click()

            # 选择【经济舱】
            cabin_trigger = driver.find_element(By.XPATH,
                                                '//div[contains(@class, "flt-subclass")]//span[text()="不限舱等"]/..')
            cabin_trigger.click()
            sleep(0.5)
            economic_option = driver.find_element(By.XPATH,
                                                  '//div[contains(@class, "class-grade-select")]//li[div[text()="经济舱"]]')
            economic_option.click()
            sleep(0.5)

            # 勾选【带儿童】
            child_checkbox = driver.find_element(By.XPATH,
                                                 '//div[contains(@class, "flt-direct")]//span[contains(text(), "带儿童")]/preceding-sibling::i')
            if "ico-checkbox-square" in child_checkbox.get_attribute("class"):
                child_checkbox.click()
            sleep(0.5)

            # 点击搜索
            driver.find_element(By.CSS_SELECTOR, "button.search-btn").click()
            sleep(8)

            # 截图
            self.take_screenshot(driver, f"CtripFlight_R001_{str(i).zfill(3)}.png")

    def test_CtripFlight_R002(self, driver):
        """需求2:非法出发地,验证错误提示"""
        illegal_cases = ["火星", "XYZ123"]
        for i, loc in enumerate(illegal_cases, 1):
            driver.get("https://www.ctrip.com")
            sleep(1)

            flight_menu = driver.find_element(By.CSS_SELECTOR, 'button[aria-label="机票 按回车键打开菜单"]')
            ActionChains(driver).move_to_element(flight_menu).perform()
            sleep(0.5)

            driver.find_element(By.LINK_TEXT, "国内/国际/中国港澳台").click()
            sleep(1)

            # 输入非法出发地
            dep_input = driver.find_element(By.NAME, "owDCity")
            dep_input.clear()
            dep_input.click()
            sleep(0.2)
            dep_input.send_keys(loc)
            sleep(3)  # 等待错误提示出现

            # 截图(即使无弹窗也需截图)
            self.take_screenshot(driver, f"CtripFlight_R002_{str(i).zfill(3)}.png")

    def test_CtripFlight_R003(self, driver):
        """需求3:国内出发 + 国际目的地,2成人1儿童1婴儿"""
        test_cases = [
            ("北京", "东京"),
            ("北京", "大阪"),
            ("上海", "东京"),
            ("上海", "大阪")
        ]
        for i, (dep, arr) in enumerate(test_cases, 1):
            driver.get("https://www.ctrip.com")
            sleep(1)

            flight_menu = driver.find_element(By.CSS_SELECTOR, 'button[aria-label="机票 按回车键打开菜单"]')
            ActionChains(driver).move_to_element(flight_menu).perform()
            sleep(0.5)

            # 点击【国内/国际/中国港澳台】
            driver.find_element(By.LINK_TEXT, "国内/国际/中国港澳台").click()
            sleep(1)

            # 确保单程选中(默认通常已选)
            one_way_label = driver.find_element(By.XPATH,
                                                '//span[contains(@class, "radio-label") and contains(text(), "单程")]')
            if "iconf-radio-unselect" in one_way_label.find_element(By.TAG_NAME, "i").get_attribute("class"):
                one_way_label.click()
            sleep(0.5)

            # 输入出发地(国内)
            dep_input = driver.find_element(By.NAME, "owDCity")
            dep_input.clear()
            dep_input.click()
            sleep(0.2)
            dep_input.send_keys(dep)
            sleep(0.5)
            dep_input.send_keys(Keys.ENTER)

            # 输入目的地(国际)
            arr_input = driver.find_element(By.NAME, "owACity")
            arr_input.clear()
            arr_input.click()
            sleep(0.2)
            arr_input.send_keys(arr)
            sleep(0.5)
            arr_input.send_keys(Keys.ENTER)

            # 点击日期输入框
            date_input = driver.find_element(By.XPATH, '//input[@aria-label="请选择日期"]')
            date_input.click()
            sleep(0.5)  # 等待日历弹出
            # 等待日历面板出现(通过“今天”按钮判断)
            calendar_panel = WebDriverWait(driver, 10).until(
                EC.presence_of_element_located(
                    (By.XPATH, '//div[contains(@class, "calendar") or contains(@class, "date-picker")]'))
            )
            # 再定位“11.12”日
            target_day = calendar_panel.find_element(By.XPATH,
                                                     '//div[contains(@class, "date-picker")]//div[contains(@class, "date-day") and .//span[@class="date-d" and text()="12"]]')
            target_day.click()

            # === 乘客类型操作(严格顺序)===
            # 点击乘客类型下拉框
            trigger = driver.find_element(By.XPATH,
                                          '//div[contains(@class, "passenger-selector")]//div[@class="form-select-v3"]')
            trigger.click()

            # 等待下拉面板出现
            WebDriverWait(driver, 10).until(
                EC.visibility_of_element_located((By.XPATH, '//div[@class="passenger-count-select"]'))
            )

            # 1. 先点“成人” +(1→2)
            adult_add = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((
                    By.XPATH,
                    '/html/body/div[1]/div[2]/div[2]/div/div[1]/div/div[1]/div/form/div/div/div/div[2]/div[3]/div/div/div/div[2]/div[1]/div[2]/div[3]'
                    # '//div[@class="passenger-count-select"]//div[text()="成人"]/ancestor::div[contains(@class, "passenger-item")]'
                    # '//div[@class="control"]/div[@class="btn"][2]'  # 第一个按钮
                    # '//div[@class="btn" and not(contains(@class, "disable"))]//i[@class="iconf-count-del"]'
                ))
            )
            adult_add.click()
            sleep(0.3)

            # 2. 再点“儿童” +
            child_add = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((
                    By.XPATH,
                    '/html/body/div[1]/div[2]/div[2]/div/div[1]/div/div[1]/div/form/div/div/div/div[2]/div[3]/div/div/div/div[2]/div[2]/div[2]/div[3]'
                    # '//div[@class="btn" and not(contains(@class, "disable"))]//i[@class="iconf-count-del"]'
                ))
            )
            child_add.click()
            sleep(0.3)

            # 3. 最后点“婴儿” +
            infant_add = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((
                    By.XPATH,
                    '/html/body/div[1]/div[2]/div[2]/div/div[1]/div/div[1]/div/form/div/div/div/div[2]/div[3]/div/div/div/div[2]/div[3]/div[2]/div[3]'
                    # '//div[@class="btn" and not(contains(@class, "disable"))]//i[@class="iconf-count-del"]'
                ))
            )
            infant_add.click()
            sleep(0.3)

            # 点击“确定”
            sure_btn = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((By.XPATH, '//a[@class="btn-sure"]'))
            )
            sure_btn.click()
            sleep(0.5)

            # 点击搜索
            search_btn = driver.find_element(By.CSS_SELECTOR, "button.search-btn")
            search_btn.click()

            # 等待航班结果加载(最多15秒)
            try:
                WebDriverWait(driver, 15).until(
                    EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'flight-item')]"))
                )
            except:
                pass  # 超时也截图

            self.take_screenshot(driver, f"CtripFlight_R003_{str(i).zfill(3)}.png")

    # test-code-end

    @staticmethod
    def take_screenshot(driver, file_name):
        timestamp = datetime.now().strftime("%H%M%S%f")
        timestamped_file_name = f"{timestamp}_{file_name}"
        screenshots_dir = "screenshots"
        if not os.path.exists(screenshots_dir):
            os.makedirs(screenshots_dir)
        screenshot_file_path = os.path.join(screenshots_dir, timestamped_file_name)
        driver.save_screenshot(screenshot_file_path)

成功经验总结

1. 放弃手动切换 Tab,依赖携程自动识别

  • 输入“东京”“大阪”后,携程自动将页面切换为国际模式
  • 无需手动点击“国际及中国港澳台”Tab,避免了元素找不到的问题;
  • 这是最核心的成功点

2. 使用绝对 XPath 定位乘客操作按钮

  • 虽然比赛规范优先推荐 ID/NAME,但在无 ID/NAME 且 class 混淆时,绝对 XPath 是合理选择
  • 通过 DevTools 复制了 /html/body/div[1]/.../div[3] 路径,精准定位到“+”按钮(实际是 iconf-count-del
  • 在比赛固定环境下,绝对路径反而更稳定。

3. 日期选择采用语义化 XPath + 显式等待

  • 使用 //span[@class="date-d" and text()="12"] 定位日期,避免依赖动态序号
  • 结合 WebDriverWait 确保日历面板加载完成;

4. 严格遵循操作顺序

  • 每次循环 重新进入首页,避免状态残留;
  • 输入框操作:clear() → click() → send_keys() → ENTER
  • 乘客操作:成人 → 儿童 → 婴儿 → 点“确定”,符合携程业务规则(婴儿 ≤ 成人),避免按钮 disable。

5. 异常处理完善

  • 使用 WebDriverWait 等待关键元素(日历、乘客面板、航班列表);

失败尝试与原因分析

手动点击“国际 Tab”

元素定位失败(XPath 不匹配)

携程会自动切换模式,无需手动操作

使用iconf-count-add定位“+”

按钮 disable,无法点击

class 名与功能相反,应通过位置[1]或绝对路径定位

send_keys("2025-09-11")输入日期

输入框readonly,无效

必须点击日历控件

依赖div[14]/div[19]等动态序号

每次运行序号变化

改用语义化 XPath 或封装等待

未等待日历/乘客面板加载

NoSuchElementException

必须加WebDriverWait

关键知识点

1. class 名与功能不一致的反爬策略

  • iconf-count-add 实际是“-”按钮;
  • iconf-count-del 实际是“+”按钮;
  • 定位时应依赖位置或绝对路径,而非 class 语义

2. 日期控件的正确操作方式

  • 只读输入框 → 必须点击日历;
  • 日历面板动态生成 → 需显式等待;
  • 日期元素通过 text() 匹配,而非序号。

3. 比赛场景下的 XPath 选择策略

  • 日常开发:相对 XPath > 绝对 XPath;
  • 比赛场景:绝对 XPath(稳定) > 相对 XPath(易变)
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐