basePage.ts 10.3 KB
import { Page, Locator, expect } from '@playwright/test';
import {test} from '@playwright/test';

/**
 * 基础页面类 - 所有页面对象的父类
 * 提供通用的页面操作方法
 */

export abstract class BasePage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  /** 
   截图并附加到Allure报告
  @param testName 测试名称
  @param screenshotName 截图名称
  */
  async attachScreenshot(testInfo:any,screenshotName:string):
  Promise<void>{
    const screenshot = await this.page.screenshot();
    await testInfo.attach(screenshotName,{
      body:screenshot,
      contentType:'image/png'
    });
  }

  /**
   * 导航到指定路径
   * @param path 相对路径
   */
  async navigate(path: string = '/'): Promise<void> {
    const baseURL = process.env.BASE_URL;
    if (!baseURL) {
      throw new Error('BASE_URL 环境变量未设置,请设置 BASE_URL 后再运行');
    }
    await this.page.goto(`${baseURL}/#${path}`);
  }

  /**
   * 等待页面加载完成
   */
  async waitForPageLoad(): Promise<void> {
    await this.page.waitForLoadState('networkidle');
  }

  /**
   * 点击元素
   * @param selector 选择器或定位器
   */
  async click(selector: string | Locator): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await locator.click();
  }

  /**
   * 点击包含指定文本的元素
   * @param text 文本内容
   */
  async clickByText(text: string): Promise<void> {
    await this.page.getByText(text).click();
  }

  /**
   * 填写输入框
   * @param selector 选择器或定位器
   * @param value 填写的值
   */
  async fill(selector: string | Locator, value: string): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await locator.fill(value);
  }

  /**
   * 清空并填写输入框
   * @param selector 选择器或定位器
   * @param value 填写的值
   */
  async clearAndFill(selector: string | Locator, value: string): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await locator.clear();
    await locator.fill(value);
  }

  /**
   * 等待元素可见
   * @param selector 选择器或定位器
   * @param timeout 超时时间(毫秒)
   */
  async waitForVisible(selector: string | Locator, timeout: number = 30000): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await locator.waitFor({ state: 'visible', timeout });
  }

  /**
   * 等待元素附加到 DOM
   * @param selector 选择器或定位器
   * @param timeout 超时时间(毫秒)
   */
  async waitForAttached(selector: string | Locator, timeout: number = 30000): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await locator.waitFor({ state: 'attached', timeout });
  }

  /**
   * 等待元素隐藏
   * @param selector 选择器或定位器
   * @param timeout 超时时间(毫秒)
   */
  async waitForHidden(selector: string | Locator, timeout: number = 30000): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await locator.waitFor({ state: 'hidden', timeout });
  }

  /**
   * 断言元素可见
   * @param selector 选择器或定位器
   */
  async expectVisible(selector: string | Locator): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await expect(locator).toBeVisible();
  }

  /**
   * 断言元素包含指定文本
   * @param selector 选择器或定位器
   * @param text 期望的文本
   */
  async expectText(selector: string | Locator, text: string | RegExp): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await expect(locator).toContainText(text);
  }

  /**
   * 断言元素具有确切文本
   * @param selector 选择器或定位器
   * @param text 期望的确切文本
   */
  async expectExactText(selector: string | Locator, text: string | RegExp): Promise<void> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    await expect(locator).toHaveText(text);
  }

  /**
   * 选择下拉列表选项(通过文本匹配)
   * @param selector 选择器
   * @param optionText 选项文本
   */
  async selectOptionByText(selector: string, optionText: string): Promise<void> {
    await this.page.locator(selector).selectOption({ label: optionText });
  }

  /**
   * 获取元素文本内容
   * @param selector 选择器或定位器
   */
  async getText(selector: string | Locator): Promise<string | null> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    return locator.textContent();
  }

  /**
   * 获取输入框的值
   * @param selector 选择器或定位器
   */
  async getInputValue(selector: string | Locator): Promise<string> {
    const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
    return locator.inputValue();
  }

  /**
   * 截图
   * @param name 截图名称
   */
  async takeScreenshot(name: string): Promise<Buffer> {
    // 使用项目根目录下的 screenshots 文件夹
    const fs = require('fs');
    const path = require('path');
    const screenshotsDir = path.join(process.cwd(), 'screenshots');
    
    // 确保 screenshots 目录存在
    if (!fs.existsSync(screenshotsDir)) {
      fs.mkdirSync(screenshotsDir, { recursive: true });
    }
    
    return this.page.screenshot({ path: path.join(screenshotsDir, `${name}.png`), fullPage: true });
  }

  /**
   * 等待指定时间
   * @param ms 毫秒数
   */
  async wait(ms: number): Promise<void> {
    await this.page.waitForTimeout(ms);
  }

  /**
   * 获取页面 URL
   */
  getUrl(): string {
    return this.page.url();
  }

  /**
   * 刷新页面
   */
  async reload(): Promise<void> {
    await this.page.reload({ waitUntil: 'networkidle' });
  }

  /**
   * 等待导航完成
   * @param timeout 超时时间
   */
  async waitForNavigation(timeout: number = 30000): Promise<void> {
    await this.page.waitForLoadState('load', { timeout });
  }

  /**
   * 获取定位器
   * @param selector 选择器
   */
  getLocator(selector: string): Locator {
    return this.page.locator(selector);
  }

  /**
   * 过滤定位器
   * @param selector 选择器
   * @param options 过滤选项
   */
  filterLocator(selector: string, options: { hasText?: string | RegExp; has?: Locator }): Locator {
    return this.page.locator(selector).filter(options);
  }

  /**
   * 填写车牌号(通用方法)
   * @param licensePlate 车牌号(如:渝ZY0706)
   * @param inputLocator 车牌号输入框定位器(可选,如果不传则需要先点击输入框)
   */
  async fillLicensePlate(licensePlate: string, inputLocator?: Locator): Promise<void> {
    // 等待页面稳定
    await this.page.waitForTimeout(500);
    
    // 如果传入了输入框定位器,先点击它
    if (inputLocator) {
      await inputLocator.click();
    }
    
    // 等待车牌键盘面板加载完成
    await this.page.waitForTimeout(1000);
    
    // 解析车牌号的省份和字母部分
    const province = licensePlate.charAt(0); // 如:渝
    const letter = licensePlate.charAt(1); // 如:Z
    const numbers = licensePlate.substring(2); // 如:Y0706
    
    // 点击省份
    await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${province}$`) }).click();
    
    // 等待字母键盘出现
    await this.page.waitForTimeout(300);
    
    // 点击第一个字母 - 智能选择,优先使用 nth(1),如果不存在则用 first
    const letterLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${letter}$`) });
    const letterCount = await letterLocator.count();
    if (letterCount > 1) {
      await letterLocator.nth(1).click();
    } else if (letterCount === 1) {
      await letterLocator.click();
    } else {
      // 回退到 getByText 方式
      await this.page.getByText(letter, { exact: true }).first().click();
    }
    
    // 逐个点击车牌号码
    for (let i = 0; i < numbers.length; i++) {
      const char = numbers[i];
      await this.page.waitForTimeout(100);
      if (char.match(/[A-Za-z]/)) {
        // 字母
        const charLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char.toUpperCase()}$`) });
        const charCount = await charLocator.count();
        if (charCount > 1) {
          await charLocator.nth(1).click();
        } else {
          await charLocator.click();
        }
      } else if (char.match(/[0-9]/)) {
        // 数字
        const digitLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) });
        const digitCount = await digitLocator.count();
        if (digitCount > 1) {
          await digitLocator.nth(1).click();
        } else {
          await digitLocator.click();
        }
      }
    }
    
    // 点击确定按钮
    await this.page.getByText('确定').click();
  }

  /**
   * 上传图片(通用方法)- 使用 filechooser 事件
   * @param imagePath 图片文件路径(相对于项目根目录)
   * @param uploadButtonLocator 上传按钮定位器(可选,默认使用 uni-scroll-view uni-button)
   */
  async uploadImage(imagePath: string, uploadButtonLocator?: Locator): Promise<void> {
    // 使用 Playwright 的 file_chooser 事件处理文件上传
    // 监听文件选择器事件
    const fileChooserPromise = this.page.waitForEvent('filechooser');
    
    // 点击上传按钮触发文件选择器
    if (uploadButtonLocator) {
      await uploadButtonLocator.click();
    } else {
      await this.page.locator('uni-scroll-view uni-button').click();
    }
    
    // 等待文件选择器出现并设置文件
    const fileChooser = await fileChooserPromise;
    await fileChooser.setFiles(imagePath);
    
    // 等待上传完成
    await this.page.waitForTimeout(1000);
  }

  /**
   * 上传图片(使用 setInputFiles 方式)
   * @param imagePath 图片文件路径
   */
  async uploadImageByInput(imagePath: string): Promise<void> {
    // 直接在 body 上设置文件
    await this.page.locator('body').setInputFiles(imagePath);
    
    // 等待上传完成
    await this.page.waitForTimeout(1000);
  }
}