一个基于 Chrome DevTools Protocol (CDP) 的浏览器扩展端到端测试框架,专为 Manifest V3 设计。
- ✅ 真实浏览器环境:在真实的 Chrome 浏览器中运行测试,而非模拟环境。
- ✅ Manifest V3 支持:专为现代浏览器扩展设计,支持 Service Worker。
- ✅ 轻量级与高性能:直接使用 CDP 协议,无需 Selenium 或 WebDriver。
- ✅ 全面的测试覆盖:
- Background (Service Worker)
- Popup & Options 页面
- 内容脚本 (Content Scripts)
- DevTools 面板
- ✅ 内置 Mock 服务器:轻松模拟后端 API 和服务。
- ✅ 网络请求拦截:拦截、修改和模拟网络请求。
- ✅ 代码覆盖率:开箱即用的代码覆盖率收集。
- ✅ CI/CD 友好:轻松集成到 GitHub Actions 或其他 CI/CD 流程中。
pnpm install
此框架被设计为 npm 包,可以在其他浏览器扩展项目中作为开发依赖项使用。
-
构建并发布框架
# 在 extest 项目目录中 pnpm run build npm publish
-
在你的扩展项目中安装
# 在你的浏览器扩展项目中 pnpm add -D extest
推荐在开发阶段使用 pnpm link
,这样可以实时查看框架的修改效果。
-
在 extest 项目中创建全局链接
# 在 extest 项目目录中 cd path/to/extest pnpm link --global
-
在你的扩展项目中使用链接
# 在你的浏览器扩展项目目录中 cd path/to/your-extension-project pnpm link --global extest
-
取消链接(可选)
当完成开发后,可以取消链接并安装正式版本:
# 在你的浏览器扩展项目中 pnpm unlink extest pnpm add -D extest
安装完成后,需要在你的扩展项目中进行配置:
-
配置 Jest
创建或修改
jest.config.js
:module.exports = { testEnvironment: 'node', globalSetup: './test/setup.js', globalTeardown: './test/teardown.js', testTimeout: 30000, };
-
创建测试设置文件
创建
test/setup.js
:const path = require('path'); const { ExtensionTestFramework } = require('extest'); module.exports = async () => { const framework = new ExtensionTestFramework(); const context = await framework.setup({ extensionPath: path.join(__dirname, '../dist'), // 你的扩展构建目录 mockServerPort: 9000, }); global.testContext = context; global.framework = framework; };
创建
test/teardown.js
:module.exports = async () => { if (global.framework) { await global.framework.teardown(); } };
-
编写测试用例
describe('Extension Tests', () => { const { driver } = global.testContext; test('should test popup functionality', async () => { const popup = await driver.openPopup(); // 你的测试逻辑 await popup.close(); }); });
在 test/setup.ts
中配置你的扩展路径:
// test/setup.ts
import path from 'path';
const extensionPath = path.join(__dirname, 'fixtures/your-extension');
// ...
创建一个新的测试文件,例如 test/my-feature.test.ts
:
describe('My Feature', () => {
// 从全局上下文中获取驱动程序
const { driver } = global.testContext;
test('should perform an action in the popup', async () => {
const popup = await driver.openPopup();
await popup.click('#my-button');
const text = await popup.$eval('#status', el => el.textContent);
expect(text).toBe('Clicked!');
await popup.close();
});
});
# 运行所有测试
pnpm test
# 运行单个测试文件
pnpm test test/my-feature.test.ts
# 监视模式
pnpm run test:watch
# 生成覆盖率报告
pnpm run test:coverage
test('should access background script functions', async () => {
const { driver } = global.testContext;
const bg = await driver.background();
// 直接调用背景脚本中的全局函数
const result = await bg.send('Runtime.evaluate', {
expression: 'myGlobalFunction()',
awaitPromise: true,
});
expect(result.result.value).toBe('expected-result');
});
test('should interact with the page via content script', async () => {
const { driver, mockServer } = global.testContext;
const page = await driver.createContentPage(`${mockServer.url}/test-page`);
// 等待内容脚本注入的元素
await page.waitForSelector('.extension-injected-element');
const text = await page.$eval('.extension-injected-element', el => el.textContent);
expect(text).toContain('Injected by');
await page.close();
});
test('should mock API requests', async () => {
const { driver, mockServer } = global.testContext;
// 启用网络模拟并定义 mock 响应
await driver.mockRequest('/api/user', { id: 1, name: 'Test User' });
const page = await driver.createContentPage();
// 在页面中发起请求
const data = await page.evaluate(async (apiUrl) => {
const response = await fetch(apiUrl);
return response.json();
}, `${mockServer.url}/api/user`);
expect(data.name).toBe('Test User');
await page.close();
});
- 原因: 异步操作未在规定时间内完成。
- 解决方案:
- 检查
await
是否被正确使用。 - 在
jest.config.js
中增加testTimeout
的值。 - 确保内容脚本有足够的时间注入,尤其是在复杂的页面上。
- 检查
- 原因: 扩展的背景、Popup 或其他页面未被正确加载。
- 解决方案:
- 验证
manifest.json
中的路径是否正确。 - 确保
waitForTarget
的超时时间足够长。
- 验证
欢迎提交 Issue 和 Pull Request!
MIT