Playwright 页面操作 #

操作概述 #

Playwright 提供丰富的页面操作方法,模拟真实用户行为。所有操作都内置自动等待机制,确保元素准备好后再执行。

自动等待机制 #

text
┌─────────────────────────────────────────────────────────────┐
│                    操作前自动检查                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 元素已附加到 DOM                                         │
│  2. 元素可见                                                 │
│  3. 元素稳定(非动画中)                                      │
│  4. 元素可接收事件                                           │
│  5. 元素已启用                                               │
│                                                             │
│  如果检查失败,会自动重试直到超时                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

点击操作 #

基本点击 #

typescript
import { test, expect } from '@playwright/test';

test('基本点击', async ({ page }) => {
  // 基本点击
  await page.click('button');
  
  // 使用 Locator
  await page.getByRole('button', { name: 'Submit' }).click();
  
  // 点击链接
  await page.getByRole('link', { name: 'Home' }).click();
});

点击选项 #

typescript
test('点击选项', async ({ page }) => {
  const button = page.getByRole('button', { name: 'Submit' });
  
  // 点击选项
  await button.click({
    button: 'left',        // 鼠标按钮: left, right, middle
    clickCount: 1,         // 点击次数
    delay: 100,            // 按下和释放之间的延迟(毫秒)
    force: false,          // 强制点击,跳过可操作性检查
    modifiers: ['Shift'],  // 修饰键: Alt, Control, Meta, Shift
    position: { x: 10, y: 10 }, // 点击位置(相对于元素)
    timeout: 30000,        // 超时时间
    trial: false,          // 试运行,不实际点击
  });
  
  // 右键点击
  await button.click({ button: 'right' });
  
  // 双击
  await button.dblclick();
  
  // Shift + 点击
  await button.click({ modifiers: ['Shift'] });
  
  // 强制点击(跳过可见性检查)
  await button.click({ force: true });
});

特殊点击场景 #

typescript
test('特殊点击场景', async ({ page }) => {
  // 点击坐标位置
  await page.mouse.click(100, 200);
  
  // 点击元素中心
  await page.locator('.card').click();
  
  // 点击元素特定位置
  await page.locator('.card').click({ position: { x: 10, y: 10 } });
  
  // 点击多个元素
  const buttons = page.getByRole('button');
  const count = await buttons.count();
  for (let i = 0; i < count; i++) {
    await buttons.nth(i).click();
  }
});

输入操作 #

文本输入 #

typescript
test('文本输入', async ({ page }) => {
  // fill - 直接填充(推荐)
  await page.fill('#username', 'john_doe');
  await page.getByLabel('Email').fill('test@example.com');
  
  // type - 逐字符输入(模拟真实打字)
  await page.type('#username', 'john_doe');
  await page.type('#username', 'john_doe', { delay: 100 }); // 每个字符延迟 100ms
  
  // pressSequentially - 按顺序按键
  await page.locator('#username').pressSequentially('john_doe');
  await page.locator('#username').pressSequentially('john_doe', { delay: 100 });
});

fill vs type #

typescript
test('fill vs type', async ({ page }) => {
  // fill - 直接设置值
  // ✅ 快速、可靠
  // ✅ 清除现有内容
  // ✅ 触发 input 和 change 事件
  await page.fill('#input', 'new value');
  
  // type - 模拟打字
  // ✅ 触发 keydown, keypress, keyup 事件
  // ✅ 适合测试实时验证
  // ⚠️ 较慢
  await page.type('#input', 'new value');
});

清除内容 #

typescript
test('清除内容', async ({ page }) => {
  const input = page.getByLabel('Email');
  
  // 清除输入框
  await input.clear();
  
  // 或使用 fill 空字符串
  await input.fill('');
  
  // 或使用快捷键
  await input.press('Control+a');
  await input.press('Backspace');
});

选择操作 #

下拉选择 #

typescript
test('下拉选择', async ({ page }) => {
  const select = page.getByRole('combobox', { name: 'Country' });
  
  // 按 value 选择
  await select.selectOption('cn');
  
  // 按 label 选择
  await select.selectOption({ label: 'China' });
  
  // 按 index 选择
  await select.selectOption({ index: 0 });
  
  // 多选
  await select.selectOption(['cn', 'us', 'uk']);
});

单选和复选 #

typescript
test('单选和复选', async ({ page }) => {
  // 复选框
  const checkbox = page.getByRole('checkbox', { name: 'Accept terms' });
  
  // 勾选
  await checkbox.check();
  
  // 取消勾选
  await checkbox.uncheck();
  
  // 断言选中状态
  await expect(checkbox).toBeChecked();
  
  // 单选按钮
  const radio = page.getByRole('radio', { name: 'Option 1' });
  await radio.check();
  await expect(radio).toBeChecked();
});

键盘操作 #

按键操作 #

typescript
test('键盘操作', async ({ page }) => {
  const input = page.getByLabel('Search');
  
  // 单个按键
  await input.press('Enter');
  await input.press('Tab');
  await input.press('Escape');
  await input.press('Backspace');
  await input.press('Delete');
  
  // 组合键
  await input.press('Control+a');     // 全选
  await input.press('Control+c');     // 复制
  await input.press('Control+v');     // 粘贴
  await input.press('Control+z');     // 撤销
  await input.press('Meta+a');        // Mac: Command+a
  
  // 方向键
  await input.press('ArrowUp');
  await input.press('ArrowDown');
  await input.press('ArrowLeft');
  await input.press('ArrowRight');
  
  // 功能键
  await input.press('F1');
  await input.press('F5');
});

键盘输入 #

typescript
test('键盘输入', async ({ page }) => {
  // 在页面上输入
  await page.keyboard.type('Hello World');
  await page.keyboard.type('Hello World', { delay: 100 });
  
  // 按下和释放
  await page.keyboard.down('Shift');
  await page.keyboard.type('hello');
  await page.keyboard.up('Shift');
  
  // 插入文本(不触发键盘事件)
  await page.keyboard.insertText('Hello World');
});

常用快捷键 #

typescript
test('常用快捷键', async ({ page }) => {
  const input = page.getByRole('textbox');
  
  // 全选
  await input.press('Control+a');
  
  // 复制
  await input.press('Control+c');
  
  // 粘贴
  await input.press('Control+v');
  
  // 剪切
  await input.press('Control+x');
  
  // 撤销
  await input.press('Control+z');
  
  // 重做
  await input.press('Control+Shift+z');
  
  // 跳到开头
  await input.press('Home');
  
  // 跳到结尾
  await input.press('End');
});

鼠标操作 #

鼠标移动 #

typescript
test('鼠标移动', async ({ page }) => {
  // 移动到指定坐标
  await page.mouse.move(100, 200);
  
  // 带步骤的移动(模拟真实移动轨迹)
  await page.mouse.move(100, 200, { steps: 10 });
  
  // 移动到元素
  await page.locator('.card').hover();
});

鼠标点击 #

typescript
test('鼠标点击', async ({ page }) => {
  // 左键点击
  await page.mouse.click(100, 200);
  
  // 右键点击
  await page.mouse.click(100, 200, { button: 'right' });
  
  // 双击
  await page.mouse.dblclick(100, 200);
  
  // 中键点击
  await page.mouse.click(100, 200, { button: 'middle' });
});

鼠标拖拽 #

typescript
test('鼠标拖拽', async ({ page }) => {
  // 方式 1: 使用 dragTo
  await page.locator('#source').dragTo(page.locator('#target'));
  
  // 方式 2: 使用鼠标操作
  await page.locator('#source').hover();
  await page.mouse.down();
  await page.locator('#target').hover();
  await page.mouse.up();
  
  // 方式 3: 精确控制
  const source = page.locator('#source');
  const target = page.locator('#target');
  
  const sourceBox = await source.boundingBox();
  const targetBox = await target.boundingBox();
  
  await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2);
  await page.mouse.down();
  await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2);
  await page.mouse.up();
});

滚动操作 #

页面滚动 #

typescript
test('页面滚动', async ({ page }) => {
  // 滚动到底部
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
  
  // 滚动到顶部
  await page.evaluate(() => window.scrollTo(0, 0));
  
  // 滚动特定距离
  await page.evaluate(() => window.scrollBy(0, 500));
  
  // 滚动到元素
  await page.locator('.footer').scrollIntoViewIfNeeded();
});

元素内滚动 #

typescript
test('元素内滚动', async ({ page }) => {
  const scrollContainer = page.locator('.scroll-container');
  
  // 滚动到元素可见
  await scrollContainer.locator('.item:last-child').scrollIntoViewIfNeeded();
  
  // 使用 evaluate 滚动
  await scrollContainer.evaluate((el) => {
    el.scrollTop = el.scrollHeight;
  });
});

文件操作 #

文件上传 #

typescript
test('文件上传', async ({ page }) => {
  // 方式 1: setInputFiles
  await page.setInputFiles('#file-input', 'test-files/document.pdf');
  
  // 上传多个文件
  await page.setInputFiles('#file-input', [
    'test-files/file1.pdf',
    'test-files/file2.pdf',
  ]);
  
  // 清除已选文件
  await page.setInputFiles('#file-input', []);
  
  // 使用 Locator
  await page.getByLabel('Upload').setInputFiles('test-files/document.pdf');
});

文件下载 #

typescript
test('文件下载', async ({ page }) => {
  // 等待下载
  const downloadPromise = page.waitForEvent('download');
  await page.getByText('Download').click();
  const download = await downloadPromise;
  
  // 获取文件名
  console.log(download.suggestedFilename());
  
  // 保存文件
  await download.saveAs('downloads/' + download.suggestedFilename());
  
  // 获取文件路径
  const path = await download.path();
  
  // 获取流
  const stream = await download.createReadStream();
});

对话框操作 #

处理 Alert #

typescript
test('处理 Alert', async ({ page }) => {
  // 监听对话框
  page.on('dialog', async dialog => {
    console.log(dialog.message());
    await dialog.accept(); // 或 dialog.dismiss()
  });
  
  // 触发 alert
  await page.evaluate(() => alert('Hello!'));
});

处理 Confirm #

typescript
test('处理 Confirm', async ({ page }) => {
  page.on('dialog', async dialog => {
    console.log(dialog.message());
    await dialog.accept(); // 点击确定
    // 或 await dialog.dismiss(); // 点击取消
  });
  
  await page.evaluate(() => confirm('Are you sure?'));
});

处理 Prompt #

typescript
test('处理 Prompt', async ({ page }) => {
  page.on('dialog', async dialog => {
    console.log(dialog.message());
    await dialog.accept('my input'); // 输入文本并确定
  });
  
  await page.evaluate(() => prompt('Enter your name:'));
});

多窗口和标签页 #

处理新窗口 #

typescript
test('处理新窗口', async ({ page }) => {
  // 监听新页面
  const pagePromise = page.waitForEvent('popup');
  
  // 触发新窗口
  await page.getByText('Open new window').click();
  
  // 获取新页面
  const newPage = await pagePromise;
  
  // 在新页面操作
  await newPage.getByLabel('Search').fill('Playwright');
  
  // 关闭新页面
  await newPage.close();
});

处理多个标签页 #

typescript
test('处理多个标签页', async ({ context }) => {
  // 创建多个页面
  const page1 = await context.newPage();
  const page2 = await context.newPage();
  
  // 在不同页面操作
  await page1.goto('https://example.com');
  await page2.goto('https://example.org');
  
  // 获取所有页面
  const pages = context.pages();
  
  // 关闭页面
  await page1.close();
  await page2.close();
});

iframe 操作 #

进入 iframe #

typescript
test('iframe 操作', async ({ page }) => {
  // 获取 iframe
  const frame = page.frameLocator('iframe[name="myframe"]');
  
  // 在 iframe 内操作
  await frame.getByLabel('Username').fill('john_doe');
  await frame.getByRole('button', { name: 'Submit' }).click();
  
  // 嵌套 iframe
  const nestedFrame = frame.frameLocator('iframe');
  await nestedFrame.getByText('Click me').click();
});

iframe 定位器 #

typescript
test('iframe 定位器', async ({ page }) => {
  // 通过 name 属性
  const frame1 = page.frameLocator('iframe[name="myframe"]');
  
  // 通过 src 属性
  const frame2 = page.frameLocator('iframe[src*="youtube"]');
  
  // 通过索引
  const frame3 = page.frameLocator('iframe').first();
  
  // 通过内容
  const frame4 = page.frameLocator('iframe').filter({ 
    has: page.locator('body:has-text("Login")') 
  });
});

等待操作 #

等待元素 #

typescript
test('等待元素', async ({ page }) => {
  // 等待元素出现
  await page.locator('.loading').waitFor({ state: 'visible' });
  
  // 等待元素消失
  await page.locator('.loading').waitFor({ state: 'hidden' });
  
  // 等待元素附加到 DOM
  await page.locator('.dynamic').waitFor({ state: 'attached' });
  
  // 等待元素从 DOM 移除
  await page.locator('.temporary').waitFor({ state: 'detached' });
  
  // 等待元素可编辑
  await page.locator('input').waitFor({ state: 'editable' });
});

等待请求 #

typescript
test('等待请求', async ({ page }) => {
  // 等待特定请求
  const responsePromise = page.waitForResponse('**/api/users');
  await page.getByText('Load Users').click();
  const response = await responsePromise;
  
  // 等待请求完成
  await page.waitForRequest('**/api/users');
  
  // 等待响应并验证
  const res = await page.waitForResponse(
    response => response.url().includes('/api/users') && response.status() === 200
  );
});

等待函数 #

typescript
test('等待函数', async ({ page }) => {
  // 等待函数返回 true
  await page.waitForFunction(() => {
    return document.querySelectorAll('.item').length > 5;
  });
  
  // 带参数的等待函数
  await page.waitForFunction(
    ({ selector, count }) => {
      return document.querySelectorAll(selector).length >= count;
    },
    { selector: '.item', count: 5 }
  );
  
  // 等待元素数量
  await expect(page.locator('.item')).toHaveCount(5);
});

截图和录制 #

截图 #

typescript
test('截图', async ({ page }) => {
  // 整页截图
  await page.screenshot({ path: 'screenshot.png' });
  
  // 元素截图
  await page.locator('.card').screenshot({ path: 'card.png' });
  
  // 全页截图(包含滚动内容)
  await page.screenshot({ 
    path: 'fullpage.png', 
    fullPage: true 
  });
  
  // 返回 Buffer
  const buffer = await page.screenshot();
  
  // 返回 Base64
  const base64 = await page.screenshot({ encoding: 'base64' });
});

录制视频 #

typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    video: 'on', // 'on' | 'off' | 'retain-on-failure' | 'on-first-retry'
  },
});

// 或在测试中手动控制
test('录制视频', async ({ page, context }) => {
  // 视频会在测试结束后自动保存
  await page.goto('/');
  await page.getByText('Click me').click();
});

最佳实践 #

1. 使用语义化操作 #

typescript
// ✅ 推荐
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('test@example.com');

// ❌ 不推荐
await page.click('#submit-btn');
await page.fill('#email-input', 'test@example.com');

2. 避免不必要的等待 #

typescript
// ✅ 推荐 - 自动等待
await page.click('button');
await expect(page.locator('.result')).toBeVisible();

// ❌ 不推荐 - 固定等待
await page.click('button');
await page.waitForTimeout(1000);

3. 使用正确的输入方法 #

typescript
// ✅ 推荐 - fill 用于普通输入
await page.getByLabel('Email').fill('test@example.com');

// ✅ 推荐 - type 用于模拟真实打字
await page.getByLabel('Search').type('Playwright', { delay: 100 });

// ✅ 推荐 - pressSequentially 用于复杂输入
await page.getByLabel('Code').pressSequentially('ABC123');

4. 处理动态内容 #

typescript
// ✅ 推荐 - 使用断言等待
await expect(page.locator('.loading')).not.toBeVisible();
await expect(page.locator('.content')).toBeVisible();

// ✅ 推荐 - 使用 waitFor
await page.locator('.loading').waitFor({ state: 'hidden' });

下一步 #

现在你已经掌握了 Playwright 页面操作的方法,接下来学习 断言验证 了解如何验证测试结果!

最后更新:2026-03-28