测试指定的Chrome版本

25 年 6 月 19 日 星期四 (已编辑)
1463 字
8 分钟

对于 Web 开发者和 QA 来说,确保应用在不同 Chrome 版本上表现一致是个老大难问题。在你的电脑上可以,在测试的电脑上不行,查下来原来是chrome版本实现不同,简直是灾难。

好在,Google给我们带来了 chrome-for-testing

Chrome For Testing: 为测试而生的

chrome-for-testing 是一系列特殊的 Chrome 二进制文件,专为 Web 应用测试和自动化设计。它的核心优势在于:

  • 版本固定:每个 chrome-for-testing 二进制文件都对应一个特定的 Chrome 发布版本(例如 116.0.5845.96)。下载了哪个版本,它就永远是哪个版本,不会在背后偷偷升级。
  • 专为自动化优化:这些版本非常适合与 Puppeteer 等浏览器自动化工具配合使用,保证了自动化脚本执行环境的一致性。
  • 官方提供:直接来自 Chrome 团队,确保了其可靠性和与标准 Chrome 的兼容性。

简单来说,chrome-for-testing 解决了测试环境中 Chrome 版本不可控的问题,让我们可以针对特定版本进行精准测试和 bug 复现。

chrome-for-testing使用git clone到本地,执行:

sh
  npm i
  npm run build

可以在dist目录查看所有的chrome版本号等内容。

Puppeteer Browsers: Chrome 二进制文件管理器

知道了 chrome-for-testing ,下一个问题就是:怎么方便地获取和管理这些特定版本的 Chrome呢?手动去网站下载可太麻烦了。

这时候就要请出 @puppeteer/browsers 这个 CLI 工具了。它是由 Puppeteer 团队维护的一个小工具,主要功能就是帮助你下载和管理各种浏览器的二进制文件,当然也包括我们重点关注的 chrome-for-testing

使用 @puppeteer/browsers 非常简单,核心命令是 install

bash
npx @puppeteer/browsers install chrome@<version>
# 例如,安装 Chrome 120.0.6099.109
npx @puppeteer/browsers install chrome@120.0.6099.109

执行后,它会自动从官方源下载指定版本的 chrome-for-testing 二进制文件到本地的一个缓存目录(通常是项目下的 .cache/puppeteer 或脚本里自定义的 ./chrome 目录)。它还会告诉你下载下来的可执行文件路径,方便后续使用。

你甚至可以用它来安装特定里程碑的版本(比如 chrome@115),它会自动选择该里程碑中最新的一个构建版本。

实用脚本

了解了 chrome-for-testing@puppeteer/browsers 后,我们可以通过编写脚本来进一步简化日常的测试流程。下面这个 run-chrome.mjs 脚本就是这样一个例子,它利用了 @puppeteer/browsers 来按需安装 Chrome 版本,并管理启动过程。

脚本核心功能:

  • 按需安装与复用:当需要特定版本的 Chrome 时,脚本会先检查本地(比如 ./chrome 目录)是否已通过 @puppeteer/browsers 下载过。如果没有,则调用 @puppeteer/browsers install 进行下载。
  • 灵活启动选项:允许你指定启动时打开的 URL 和自定义的用户数据目录,方便创建隔离的测试会话。
  • 默认最新:如果不指定版本,会尝试使用本地已安装的最新版本。

run-chrome.mjs 如何利用 @puppeteer/browsers

在脚本的 installChrome 函数中,关键的安装动作就是通过 child_process.spawn 执行了 npx @puppeteer/browsers install chrome@${version} 命令。脚本随后会解析该命令的输出来获取 Chrome 的可执行路径。

javascript

import { spawn } from 'child_process';
import { readdir, access } from 'fs/promises';
import path from 'path';

// A simple version comparison function
function compareVersions(v1, v2) {
  const parts1 = v1.split('.').map(Number);
  const parts2 = v2.split('.').map(Number);
  const len = Math.max(parts1.length, parts2.length);
  for (let i = 0; i < len; i++) {
    const p1 = parts1[i] || 0;
    const p2 = parts2[i] || 0;
    if (p1 > p2) return 1;
    if (p1 < p2) return -1;
  }
  return 0;
}

async function installChrome(version) {
  return new Promise((resolve, reject) => {
    console.log(`Installing Chrome version ${version}...`);
    const installProcess = spawn(
      'npx',
      ['@puppeteer/browsers', 'install', `chrome@${version}`],
      { shell: true }
    );

    let stdoutData = '';
    installProcess.stdout.on('data', (data) => {
      const output = data.toString();
      process.stdout.write(output); // Show output to user in real-time
      stdoutData += output;
    });

    installProcess.stderr.on('data', (data) => {
      process.stderr.write(data.toString());
    });

    installProcess.on('close', (code) => {
      if (code !== 0) {
        return reject(new Error(`Failed to install Chrome version ${version}. Exit code: ${code}`));
      }
      console.log(`\nInstallation of version ${version} complete.`);
      // Regex to find the line with the path and capture the path itself
      const match = stdoutData.match(/^chrome@.*? (.*)$/m);
      if (!match || !match[1]) {
        return reject(new Error('Could not find Chrome executable path in the installation logs.'));
      }
      const executablePath = match[1].trim();
      resolve(executablePath);
    });

    installProcess.on('error', (err) => {
      reject(new Error(`Failed to start installation process for version ${version}.`, err));
    });
  });
}

function launchChrome(executablePath, openUrl, userDir = "../../test-chrome-data") {
  const absolutePath = path.resolve(executablePath);
  console.log(`Launching Chrome from: ${absolutePath}`);
  const args = [];
  if (userDir) {
    args.push(`--user-data-dir=${userDir}`);
  }
  if (openUrl) {
    args.push(openUrl);
  }
  const chromeProcess = spawn(absolutePath, args, {
    stdio: 'ignore',
    detached: true,
  });
  chromeProcess.on('error', (err) => {
    console.error(`Failed to launch Chrome at ${absolutePath}`, err);
    process.exit(1);
  });
  chromeProcess.unref();
  console.log('Chrome launched successfully.');
}

async function findInstalledVersion(partialVersion) {
    const chromeDir = './chrome';
    const platformPrefix = 'win64-';
    try {
        const files = await readdir(chromeDir);
        const matchingVersions = files
            .filter(file => file.startsWith(`${platformPrefix}${partialVersion}`))
            .map(file => file.substring(platformPrefix.length))
            .sort(compareVersions)
            .reverse();
        return matchingVersions.length > 0 ? matchingVersions[0] : null;
    } catch (e) {
        if (e.code === 'ENOENT') {
            return null; // chrome directory doesn't exist
        }
        throw e;
    }
}

async function run() {
  try {
    const chromeDir = './chrome';
    const platformPrefix = 'win64-'; // Assuming windows
    const args = process.argv.slice(2);
    let versionArg;
    let openUrl;
    let userDir = path.resolve('./userdata');

    for (let i = 0; i < args.length; i++) {
      const arg = args[i];
      if (arg === '--open') {
        openUrl = args[++i];
      } else if (arg === '--user_dir') {
        userDir = args[++i];
      } else if (!arg.startsWith('--')) {
        versionArg = arg;
      }
    }

    let executablePath;

    if (versionArg) {
      console.log(`Version ${versionArg} specified.`);
      const fullVersion = await findInstalledVersion(versionArg);
      if (fullVersion) {
        console.log(`Found installed version matching "${versionArg}": ${fullVersion}`);
        executablePath = path.join(chromeDir, `${platformPrefix}${fullVersion}`, 'chrome-win64', 'chrome.exe');
      } else {
        console.log(`Chrome version matching "${versionArg}" is not installed.`);
        executablePath = await installChrome(versionArg);
      }
    } else {
      console.log('No version specified, finding latest installed version...');
      const files = await readdir(chromeDir).catch(() => []);
      const versions = files
        .filter(file => file.startsWith(platformPrefix))
        .map(file => file.substring(platformPrefix.length))
        .sort(compareVersions)
        .reverse();

      if (versions.length === 0) {
        console.error('No installed Chrome versions found and no version specified.');
        console.error('Usage: node run.mjs [version] [--open <url>] [--user_dir <path>]');
        console.error('Example: node run.mjs 138.0.7163.0 --open https://www.google.com');
        process.exit(1);
      }
      const latestVersion = versions[0];
      console.log(`Using latest found version: ${latestVersion}`);
      executablePath = path.join(chromeDir, `${platformPrefix}${latestVersion}`, 'chrome-win64', 'chrome.exe');
    }

    launchChrome(executablePath, openUrl, userDir);

  } catch (error) {
    console.error('An error occurred:', error.message);
    process.exit(1);
  }
}

run();


使用

用法保持不变,通过 Node.js 在终端执行:

bash
node run-chrome.mjs [版本号] [--open <URL>] [--user_dir <路径>]

示例:

要下载并运行 138.0.7163.0 版 Chrome:

bash
node run-chrome.mjs 138.0.7163.0

用户数据默认放在"../../test-chrome-data"目录,也可以用参数指定打开的初始url和用户数据的目录:

bash
node run-chrome.mjs 138.0.7163.0 --open https://www.google.com --user_dir D:\test-chrome-data

文章标题:测试指定的Chrome版本

文章作者:shirtiny

文章链接:https://kizamu.anror.com/posts/chrome-for-testing[复制]

最后修改时间:


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。