对于 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到本地,执行:
npm i
npm run build
可以在dist目录查看所有的chrome版本号等内容。
Puppeteer Browsers: Chrome 二进制文件管理器
知道了 chrome-for-testing ,下一个问题就是:怎么方便地获取和管理这些特定版本的 Chrome呢?手动去网站下载可太麻烦了。
这时候就要请出 @puppeteer/browsers 这个 CLI 工具了。它是由 Puppeteer 团队维护的一个小工具,主要功能就是帮助你下载和管理各种浏览器的二进制文件,当然也包括我们重点关注的 chrome-for-testing。
使用 @puppeteer/browsers 非常简单,核心命令是 install:
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 的可执行路径。
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 在终端执行:
node run-chrome.mjs [版本号] [--open <URL>] [--user_dir <路径>]
示例:
要下载并运行 138.0.7163.0 版 Chrome:
node run-chrome.mjs 138.0.7163.0
用户数据默认放在"../../test-chrome-data"目录,也可以用参数指定打开的初始url和用户数据的目录:
node run-chrome.mjs 138.0.7163.0 --open https://www.google.com --user_dir D:\test-chrome-data