Playwright 第7章:高级页面操作

在掌握基础页面操作后,本章介绍 Playwright 的高级交互能力,包括键盘/鼠标事件、拖拽操作、滚动控制、下拉框选择、文件输入和 iframe 操作等。

键盘事件

Playwright 提供完整的键盘模拟能力,支持组合键、功能键和特殊键。

键盘基础操作

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');

// 按下和释放单个键
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.press('Tab');

// 组合键
await page.keyboard.press('Control+A');      // 全选
await page.keyboard.press('Control+C');      // 复制
await page.keyboard.press('Control+V');      // 粘贴
await page.keyboard.press('Control+Shift+I'); // 打开开发者工具

// 逐字符输入
await page.keyboard.type('Hello World');
await page.keyboard.type('Hello', { delay: 50 }); // 带延迟输入

// 按下和释放分开控制
await page.keyboard.down('Shift');
await page.keyboard.press('KeyA');
await page.keyboard.press('KeyB');
await page.keyboard.up('Shift'); // 输出 "AB"

// 特殊键
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowUp');
await page.keyboard.press('PageDown');
await page.keyboard.press('Home');
await page.keyboard.press('End');
await page.keyboard.press('Delete');
await page.keyboard.press('Backspace');

await browser.close();

常用键盘快捷键

组合键操作
Control+A全选
Control+C复制
Control+V粘贴
Control+X剪切
Control+Z撤销
Control+Shift+Z重做
Control+S保存
Control+F查找

鼠标事件

鼠标基础操作

const page = await browser.newPage();
await page.goto('https://example.com');

// 点击
await page.mouse.click(100, 200);              // 左键点击坐标 (100, 200)
await page.mouse.click(100, 200, { button: 'right' }); // 右键

// 双击
await page.mouse.dblclick(100, 200);

// 移动鼠标
await page.mouse.move(0, 0);
await page.mouse.move(500, 300, { steps: 10 }); // 分 10 步移动(产生轨迹)

// 按下和释放
await page.mouse.down({ button: 'left' });
await page.mouse.move(300, 400, { steps: 5 });
await page.mouse.up({ button: 'left' });

// 滚动
await page.mouse.wheel(0, 500);  // 向下滚动 500px
await page.mouse.wheel(0, -200); // 向上滚动 200px

拖拽操作

使用 dragAndDrop 方法

const page = await browser.newPage();
await page.goto('https://example.com/drag-drop');

// 方法一:dragAndrop(推荐)
const source = page.locator('.draggable-item');
const target = page.locator('.drop-zone');
await source.dragTo(target);

// 带选项的拖拽
await source.dragTo(target, {
  sourcePosition: { x: 0, y: 0 },    // 源元素相对偏移
  targetPosition: { x: 50, y: 50 },   // 目标元素相对偏移
  force: true,                        // 强制拖拽
  timeout: 5000,
});

// 方法二:手动拖拽(适用于特殊场景)
await page.mouse.move(100, 200);
await page.mouse.down();
await page.mouse.move(300, 400, { steps: 20 });
await page.mouse.up();

文件拖拽上传

// 模拟拖拽文件到上传区域
const uploadZone = page.locator('.upload-zone');

// 创建 DataTransfer 事件
await page.evaluate(() => {
  const dropZone = document.querySelector('.upload-zone');

  // 创建拖拽事件
  const dragEnterEvent = new DragEvent('dragenter', {
    dataTransfer: new DataTransfer(),
  });
  dropZone.dispatchEvent(dragEnterEvent);

  const dropEvent = new DragEvent('drop', {
    dataTransfer: new DataTransfer(),
  });
  dropZone.dispatchEvent(dropEvent);
});

// 直接使用 fileChooser(更可靠)
const [fileChooser] = await Promise.all([
  page.waitForEvent('filechooser'),
  page.locator('.upload-zone').click(),
]);
await fileChooser.setFiles('document.pdf');

滚动操作

const page = await browser.newPage();
await page.goto('https://example.com/long-page');

// 滚动到元素位置
await page.locator('#footer').scrollIntoViewIfNeeded();

// 使用 evaluate 滚动
await page.evaluate(() => {
  window.scrollTo(0, document.body.scrollHeight); // 滚到底部
});

await page.evaluate(() => {
  window.scrollTo(0, 0); // 滚到顶部
});

// 按像素滚动
await page.evaluate(() => {
  window.scrollBy(0, 500); // 向下滚动 500px
});

// 滚动到特定元素
await page.locator('.load-more').scrollIntoViewIfNeeded();
await page.locator('.load-more').click();

// 水平滚动
await page.evaluate(() => {
  window.scrollTo(500, 0); // 水平滚动 500px
});

下拉框高级选择

const page = await browser.newPage();

// 标准 select 元素
await page.selectOption('#city', 'beijing');
await page.selectOption('#city', { label: '北京' });
await page.selectOption('#city', { index: 1 });

// 多选 select
await page.selectOption('#hobbies', ['reading', 'music']);

// 自定义下拉框(非 select 元素)
await page.click('.custom-dropdown');
await page.click('.dropdown-option[data-value="option1"]');

// 搜索选择(带搜索框的下拉框)
await page.click('.search-select');
await page.fill('.search-input', '北京');
await page.click('.search-result:first-child');

// 验证选择结果
const selectedValue = await page.locator('#city').inputValue();
console.log('选中值:', selectedValue);

文件输入

fileChooser 处理

const page = await browser.newPage();

// 方法一:直接设置输入文件(最可靠)
await page.setInputFiles('#file-input', 'document.pdf');

// 多个文件
await page.setInputFiles('#file-input', [
  'file1.pdf',
  'file2.jpg',
  'file3.png',
]);

// 方法二:通过 fileChooser 事件
const [fileChooser] = await Promise.all([
  page.waitForEvent('filechooser'),
  page.click('#upload-btn'),
]);

// 设置文件
await fileChooser.setFiles('photo.jpg');

// 多个文件
await fileChooser.setFiles(['photo1.jpg', 'photo2.png']);

// 取消文件选择
await fileChooser.cancel();

// 检查文件选择器状态
console.log('是否选择多个:', fileChooser.isMultiple());

iframe 操作

处理嵌入式 iframe

const page = await browser.newPage();
await page.goto('https://example.com/with-iframe');

// 方法一:通过 locator 获取 iframe
const iframe = page.frameLocator('#embedded-iframe');

// 在 iframe 中操作
await iframe.locator('#username').fill('admin');
await iframe.locator('.submit-btn').click();
const text = await iframe.locator('.result').textContent();
console.log('iframe 内容:', text);

// 方法二:通过 frame 对象
const frame = page.frame({ name: 'iframe-name' });
// 或
const frame2 = page.frame({ url: '**/iframe-content.html' });

if (frame) {
  await frame.fill('#email', '[email protected]');
  await frame.click('button');
}

// 获取 iframe 内的标题
const frameTitle = await iframe.locator('h1').textContent();

// 等待 iframe 加载完成
await iframe.locator('.content').waitFor({ state: 'visible' });

// 多层 iframe 嵌套
const innerIframe = iframe.frameLocator('.inner-iframe');
await innerIframe.locator('.inner-button').click();

// 获取所有 iframe
const allFrames = page.frames();
console.log('页面框架数:', allFrames.length);
for (const f of allFrames) {
  console.log('框架URL:', f.url());
}

弹窗处理

const page = await browser.newPage();

// 处理 alert/confirm/prompt 弹窗
page.on('dialog', async (dialog) => {
  console.log('弹窗类型:', dialog.type());
  console.log('弹窗消息:', dialog.message());

  switch (dialog.type()) {
    case 'alert':
      await dialog.accept();
      break;
    case 'confirm':
      await dialog.accept();   // 确认
      // await dialog.dismiss(); // 取消
      break;
    case 'prompt':
      await dialog.accept('输入的值');
      break;
    case 'beforeunload':
      await dialog.accept();
      break;
  }
});

// 触发弹窗
await page.click('#show-alert');
await page.click('#show-confirm');
await page.click('#show-prompt');

总结

高级页面操作让 Playwright 能够处理复杂的交互场景。键盘和鼠标事件提供了底层的操作能力,拖拽操作适用于 Web 应用的拖拽功能测试。iframe 操作覆盖了嵌套页面场景,弹窗处理使自动化脚本能够应对各种对话框。在实际项目中,建议根据场景选择最合适的操作方法,优先使用高层次的 API(如 dragTo 而非手动鼠标事件),以获得更好的可靠性。