Files
2026-07-13 21:36:47 +08:00

15 KiB
Raw Permalink Blame History

运行自定义 Playwright 代码

何时使用:当 CLI 命令不够用时——地理定位、权限、媒体模拟、等待策略、iframe 交互、文件下载、剪贴板访问、复杂的多步骤工作流,或任何需要完整 Playwright API 的场景。 前置条件core-commands.md 了解基本的 CLI 用法

快速参考

# 语法:run-code 接受一个以 page 为参数的 async 函数
playwright-cli run-code "async page => {
  // 此处可使用完整的 Playwright API
  // page.context() 用于浏览器上下文操作
  // 返回一个值即可在输出中查看
  return await page.title();
}"

地理定位

覆盖浏览器报告的位置——对于测试基于位置的功能(如门店查找器、配送区域和天气应用)至关重要。

# 授予权限并将位置设置为纽约
playwright-cli run-code "async page => {
  await page.context().grantPermissions(['geolocation']);
  await page.context().setGeolocation({ latitude: 40.7128, longitude: -74.0060 });
}"

# 伦敦
playwright-cli run-code "async page => {
  await page.context().grantPermissions(['geolocation']);
  await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
}"

# 旧金山
playwright-cli run-code "async page => {
  await page.context().grantPermissions(['geolocation']);
  await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
}"

# 东京
playwright-cli run-code "async page => {
  await page.context().grantPermissions(['geolocation']);
  await page.context().setGeolocation({ latitude: 35.6762, longitude: 139.6503 });
}"

# 在会话中更新位置(模拟用户移动)
playwright-cli run-code "async page => {
  await page.context().setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
}"

# 清除地理定位覆盖
playwright-cli run-code "async page => {
  await page.context().clearPermissions();
}"

权限

控制浏览器权限授予——通知、摄像头、麦克风、剪贴板等。

# 授予多个权限
playwright-cli run-code "async page => {
  await page.context().grantPermissions([
    'geolocation',
    'notifications',
    'camera',
    'microphone'
  ]);
}"

# 仅针对特定来源授予权限
playwright-cli run-code "async page => {
  await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], {
    origin: 'https://example.com'
  });
}"

# 撤销所有权限
playwright-cli run-code "async page => {
  await page.context().clearPermissions();
}"

可用权限:geolocationnotificationscameramicrophoneclipboard-readclipboard-writepayment-handlermidimidi-sysexambient-light-sensoraccelerometergyroscopemagnetometeraccessibility-eventsbackground-sync

媒体模拟

测试你的应用在不同媒体条件下的表现——深色模式、减弱动效、打印布局。

色彩方案

# 深色模式
playwright-cli run-code "async page => {
  await page.emulateMedia({ colorScheme: 'dark' });
}"

# 浅色模式
playwright-cli run-code "async page => {
  await page.emulateMedia({ colorScheme: 'light' });
}"

# 系统偏好(无覆盖)
playwright-cli run-code "async page => {
  await page.emulateMedia({ colorScheme: 'no-preference' });
}"

减弱动效

# 模拟 prefers-reduced-motion: reduce
playwright-cli run-code "async page => {
  await page.emulateMedia({ reducedMotion: 'reduce' });
}"

# 重置为无偏好
playwright-cli run-code "async page => {
  await page.emulateMedia({ reducedMotion: 'no-preference' });
}"

强制色彩(高对比度)

playwright-cli run-code "async page => {
  await page.emulateMedia({ forcedColors: 'active' });
}"

打印媒体

# 模拟打印样式表(在生成 PDF 前使用)
playwright-cli run-code "async page => {
  await page.emulateMedia({ media: 'print' });
}"

# 重置为屏幕模式
playwright-cli run-code "async page => {
  await page.emulateMedia({ media: 'screen' });
}"

组合多个模拟

playwright-cli run-code "async page => {
  await page.emulateMedia({
    colorScheme: 'dark',
    reducedMotion: 'reduce',
    forcedColors: 'none'
  });
}"

区域设置和时区

覆盖浏览器区域设置和时区以测试国际化:

# 设置区域设置(影响数字格式、日期显示等)
playwright-cli run-code "async page => {
  // 区域设置在上下文创建时设定;对于现有上下文,使用 evaluate
  return await page.evaluate(() => navigator.language);
}"

# 对于新上下文,在打开时设置区域设置和时区:
# playwright-cli open --config=locale-config.json
# 其中 locale-config.json 包含:{ "locale": "de-DE", "timezoneId": "Europe/Berlin" }

等待策略

当页面在操作后需要时间加载、渲染或稳定时使用。

等待加载状态

# 等待所有网络请求完成
playwright-cli run-code "async page => {
  await page.waitForLoadState('networkidle');
}"

# 等待 DOM 内容加载完成
playwright-cli run-code "async page => {
  await page.waitForLoadState('domcontentloaded');
}"

# 等待完全加载(包括图片、样式表)
playwright-cli run-code "async page => {
  await page.waitForLoadState('load');
}"

等待元素

# 等待加载旋转器消失
playwright-cli run-code "async page => {
  await page.waitForSelector('.loading-spinner', { state: 'hidden' });
}"

# 等待内容出现
playwright-cli run-code "async page => {
  await page.waitForSelector('.search-results', { state: 'visible', timeout: 10000 });
}"

# 等待元素从 DOM 中完全移除
playwright-cli run-code "async page => {
  await page.waitForSelector('.skeleton-loader', { state: 'detached' });
}"

等待 URL 变化

playwright-cli run-code "async page => {
  await page.waitForURL('**/dashboard');
}"

playwright-cli run-code "async page => {
  await page.waitForURL(/.*\/order\/\d+/);
}"

等待自定义条件

# 等待 JavaScript 变量被设置
playwright-cli run-code "async page => {
  await page.waitForFunction(() => window.appReady === true);
}"

# 等待特定数量的元素
playwright-cli run-code "async page => {
  await page.waitForFunction(() => document.querySelectorAll('.item').length >= 10);
}"

# 轮询等待
playwright-cli run-code "async page => {
  await page.waitForFunction(
    () => document.querySelector('.status')?.textContent === 'Complete',
    { polling: 500, timeout: 30000 }
  );
}"

等待网络请求

# 等待特定 API 调用完成
playwright-cli run-code "async page => {
  const responsePromise = page.waitForResponse('**/api/users');
  await page.click('button#load-users');
  const response = await responsePromise;
  return { status: response.status(), url: response.url() };
}"

# 等待发起请求
playwright-cli run-code "async page => {
  const requestPromise = page.waitForRequest('**/api/submit');
  await page.click('button#submit');
  const request = await requestPromise;
  return request.postDataJSON();
}"

框架和 Iframe

与 iframe 内部的内容交互:

# 点击 iframe 内的按钮
playwright-cli run-code "async page => {
  const frame = page.locator('iframe#my-iframe').contentFrame();
  await frame.locator('button.submit').click();
}"

# 填写 iframe 内的表单
playwright-cli run-code "async page => {
  const frame = page.locator('iframe[name=\"checkout\"]').contentFrame();
  await frame.locator('input[name=\"card-number\"]').fill('4111111111111111');
  await frame.locator('input[name=\"expiry\"]').fill('12/25');
  await frame.locator('input[name=\"cvc\"]').fill('123');
}"

# 获取所有框架的 URL
playwright-cli run-code "async page => {
  const frames = page.frames();
  return frames.map(f => ({ name: f.name(), url: f.url() }));
}"

# 嵌套 iframe
playwright-cli run-code "async page => {
  const outerFrame = page.locator('iframe#outer').contentFrame();
  const innerFrame = outerFrame.locator('iframe#inner').contentFrame();
  await innerFrame.locator('button').click();
}"

文件下载

# 触发下载并保存文件
playwright-cli run-code "async page => {
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('a.download-link')
  ]);
  const filename = download.suggestedFilename();
  await download.saveAs('./downloads/' + filename);
  return '已下载:' + filename;
}"

# 自定义路径下载
playwright-cli run-code "async page => {
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.getByRole('button', { name: 'Export CSV' }).click()
  ]);
  await download.saveAs('/tmp/export.csv');
  return '已保存至 /tmp/export.csv';
}"

# 获取下载 URL(不保存)
playwright-cli run-code "async page => {
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('#download-btn')
  ]);
  return download.url();
}"

剪贴板

# 读取剪贴板内容
playwright-cli run-code "async page => {
  await page.context().grantPermissions(['clipboard-read']);
  return await page.evaluate(() => navigator.clipboard.readText());
}"

# 写入剪贴板
playwright-cli run-code "async page => {
  await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!');
}"

# 将元素中的文本复制到剪贴板
playwright-cli run-code "async page => {
  const text = await page.locator('.api-key').textContent();
  await page.evaluate(t => navigator.clipboard.writeText(t), text);
  return '已复制:' + text;
}"

页面信息

# 页面标题
playwright-cli run-code "async page => {
  return await page.title();
}"

# 当前 URL
playwright-cli run-code "async page => {
  return page.url();
}"

# 完整页面 HTML 内容
playwright-cli run-code "async page => {
  return await page.content();
}"

# 视口大小
playwright-cli run-code "async page => {
  return page.viewportSize();
}"

# 浏览器信息
playwright-cli run-code "async page => {
  return await page.evaluate(() => ({
    userAgent: navigator.userAgent,
    language: navigator.language,
    languages: navigator.languages,
    cookiesEnabled: navigator.cookieEnabled,
    onLine: navigator.onLine,
    platform: navigator.platform,
    screenSize: { width: screen.width, height: screen.height }
  }));
}"

JavaScript 执行

# 执行并返回结果
playwright-cli run-code "async page => {
  return await page.evaluate(() => {
    return {
      title: document.title,
      url: window.location.href,
      elementCount: document.querySelectorAll('*').length,
      scripts: document.querySelectorAll('script').length
    };
  });
}"

# 向 evaluate 传递参数
playwright-cli run-code "async page => {
  const selector = '.product-card';
  const count = await page.evaluate(
    sel => document.querySelectorAll(sel).length,
    selector
  );
  return '找到了 ' + count + ' 个产品';
}"

# 修改 DOM
playwright-cli run-code "async page => {
  await page.evaluate(() => {
    document.querySelector('.banner')?.remove();
    document.body.style.zoom = '80%';
  });
}"

错误处理

# 对可选元素使用 try-catch
playwright-cli run-code "async page => {
  try {
    await page.click('.cookie-consent-accept', { timeout: 2000 });
    return 'Cookie 横幅已关闭';
  } catch (e) {
    return '未找到 Cookie 横幅';
  }
}"

# 重试模式
playwright-cli run-code "async page => {
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      await page.click('.flaky-button', { timeout: 3000 });
      return '在第 ' + attempt + ' 次尝试时点击成功';
    } catch (e) {
      if (attempt === 3) throw e;
      await page.waitForTimeout(1000);
    }
  }
}"

复杂工作流

登录并保存认证状态

playwright-cli run-code "async page => {
  await page.goto('https://example.com/login');
  await page.fill('input[name=email]', 'user@example.com');
  await page.fill('input[name=password]', 'secret');
  await page.click('button[type=submit]');
  await page.waitForURL('**/dashboard');
  await page.context().storageState({ path: 'auth.json' });
  return '登录成功,状态已保存至 auth.json';
}"

从多个页面抓取数据

playwright-cli run-code "async page => {
  const results = [];
  for (let i = 1; i <= 5; i++) {
    await page.goto(\`https://example.com/products?page=\${i}\`);
    await page.waitForSelector('.product-card');
    const items = await page.locator('.product-card').evaluateAll(cards =>
      cards.map(card => ({
        name: card.querySelector('.title')?.textContent?.trim(),
        price: card.querySelector('.price')?.textContent?.trim(),
        rating: card.querySelector('.rating')?.getAttribute('data-value')
      }))
    );
    results.push(...items);
  }
  return JSON.stringify(results, null, 2);
}"

填写多步骤表单(向导)

playwright-cli run-code "async page => {
  // 步骤 1:个人信息
  await page.fill('#firstName', 'Jane');
  await page.fill('#lastName', 'Doe');
  await page.fill('#email', 'jane@example.com');
  await page.click('button:text(\"Next\")');

  // 步骤 2:地址
  await page.waitForSelector('#street');
  await page.fill('#street', '123 Main St');
  await page.fill('#city', 'Springfield');
  await page.selectOption('#state', 'IL');
  await page.fill('#zip', '62701');
  await page.click('button:text(\"Next\")');

  // 步骤 3:确认并提交
  await page.waitForSelector('.review-summary');
  await page.click('button:text(\"Submit\")');
  await page.waitForURL('**/confirmation');

  return '表单提交成功';
}"

无限滚动数据提取

playwright-cli run-code "async page => {
  await page.goto('https://example.com/feed');
  const items = [];

  while (items.length < 50) {
    const newItems = await page.locator('.feed-item').evaluateAll(
      els => els.map(el => el.textContent.trim())
    );
    items.push(...newItems.filter(item => !items.includes(item)));

    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
    await page.waitForTimeout(1000);

    // 检查是否已到达底部
    const hasMore = await page.locator('.load-more').isVisible().catch(() => false);
    if (!hasMore && newItems.length === items.length) break;
  }

  return items.slice(0, 50);
}"

提示

  • 返回值:始终从 run-codereturn 数据,以便在 CLI 输出中查看
  • page.context():访问浏览器上下文以操作权限、cookies、存储、地理定位
  • 错误信息Playwright 错误包含元素快照和调用日志——请仔细阅读
  • 与 CLI 结合使用:使用 run-code 进行设置(权限、路由),然后使用 CLI 命令进行交互
  • Async/await:所有 Playwright 操作都是异步的——务必使用 await