这样做有好办法吗?我正在编写一个与网站交互作为内容脚本的扩展程序,并使用localstorage保存数据。是否有可用于测试此行为的工具,框架等?我意识到有一些用于测试javascript的通用工具,但是那些足以测试扩展的能力吗?单元测试是最重要的,但我也对其他类型的测试(例如集成测试)感兴趣。
答案 0 :(得分:103)
是的,现有的框架非常有用..
在最近的过去,我已将所有测试放在嵌入到应用程序中的“测试”页面中,但除非是物理输入,否则无法访问。
例如,我可以在chrome-extension://asdasdasdasdad/unittests.html
测试可以访问localStorage
等。对于访问内容脚本,理论上你可以通过测试页面中的嵌入式IFRAME测试,但是这些是更多的集成级别测试,单元测试需要你抽象远离实际页面,以便您不依赖它们,同样可以访问localStorage。
如果您想直接测试页面,可以编排扩展程序以打开新标签(chrome.tab.create({“url”:“someurl”})。对于每个新标签页,您的内容脚本应该运行,您可以使用您的测试框架来检查您的代码是否已完成它应该做的事情。
答案 1 :(得分:55)
在处理多个Chrome扩展程序时,我提出了sinon-chrome
项目,该项目允许使用mocha
,nodejs
和phantomjs
运行单元测试。
Basicaly,它创建了所有chrome.*
API的sinon模拟,您可以在其中放置任何预定义的json响应。
接下来,您使用节点的vm.runInNewContext
加载背景页面和phantomjs
渲染弹出/选项页面。
最后,你断言用所需的参数调用chrome api。
我们举一个例子:
假设我们有简单的chrome扩展,在按钮徽章中显示已打开的标签数量。
后台页面:
chrome.tabs.query({}, function(tabs) {
chrome.browserAction.setBadgeText({text: String(tabs.length)});
});
为了测试我们需要:
chrome.tabs.query
以返回预定义的响应,例如两个标签。chrome.*
api注入某个环境代码段如下:
const vm = require('vm');
const fs = require('fs');
const chrome = require('sinon-chrome');
// 1. mock `chrome.tabs.query` to return predefined response
chrome.tabs.query.yields([
{id: 1, title: 'Tab 1'},
{id: 2, title: 'Tab 2'}
]);
// 2. inject our mocked chrome.* api into some environment
const context = {
chrome: chrome
};
// 3. run our extension code in this environment
const code = fs.readFileSync('src/background.js');
vm.runInNewContext(code, context);
// 4. assert that button badge equals to '2'
sinon.assert.calledOnce(chrome.browserAction.setBadgeText);
sinon.assert.calledWithMatch(chrome.browserAction.setBadgeText, {
text: "2"
});
现在我们可以将它包装到mocha的describe..it
函数中并从终端运行:
$ mocha
background page
✓ should display opened tabs count in button badge
1 passing (98ms)
您可以找到完整示例here。
此外,sinon-chrome允许使用预定义的响应触发任何chrome事件,例如
chrome.tab.onCreated.trigger({url: 'http://google.com'});
答案 2 :(得分:2)
关于Chrome中已有的工具:
在Chrome开发人员工具中,有适用于本地存储的资源部分。
开发人员工具>资源>本地存储
查看当地存储的变化。
您可以使用console.profile来测试性能并观察运行时调用堆栈。
如果您在没有后台页面/脚本且没有消息传递的情况下一起使用内容脚本和本地存储,则只能从该站点访问本地存储。 因此,要测试这些页面,您必须在这些选项卡中注入测试脚本。
答案 3 :(得分:1)
虽然sinon.js
似乎运行良好,但您也可以使用简单的Jasmine并模拟您需要的Chrome回调。例如:
chrome = {
runtime: {
onMessage : {
addListener : function() {}
}
}
}
describe("JSGuardian", function() {
describe("BlockCache", function() {
beforeEach(function() {
this.blockCache = new BlockCache();
});
it("should recognize added urls", function() {
this.blockCache.add("http://some.url");
expect(this.blockCache.allow("http://some.url")).toBe(false);
});
} // ... etc
只需修改默认SpecRunner.html
即可运行您的代码。
答案 4 :(得分:0)
我发现我可以使用 Selenium网络驱动程序来启动预装扩展程序的新浏览器实例,并点击pyautogui - 因为Selenium无法驱动扩展程序的“视图”。点击后你可以制作截图并将它们与'预期'相比较,期望95%的相似性(因为在不同的浏览器上,标记运动可以接受几个像素)。
答案 5 :(得分:0)
要确认之前的几个答案,Jasmine似乎可以与Chrome扩展程序很好地配合使用。我正在使用3.4.0版。
您可以使用Jasmine spies轻松为各种API创建测试双打。无需从头开始构建自己的。例如:
describe("Test suite", function() {
it("Test case", function() {
// Set up spies and fake data.
spyOn(chrome.browserAction, "setPopup");
spyOn(chrome.identity, "removeCachedAuthToken");
fakeToken = "faketoken-faketoken-faketoken";
fakeWindow = jasmine.createSpyObj("window", ["close"]);
// Call the function under test.
logout(fakeWindow, fakeToken);
// Perform assertions.
expect(chrome.browserAction.setPopup).toHaveBeenCalledWith({popup: ""});
expect(chrome.identity.removeCachedAuthToken).toHaveBeenCalledWith({token: fakeToken});
expect(fakeWindow.close.calls.count()).toEqual(1);
});
});
更多详细信息(如果有帮助的话):
正如另一个答案所述,我创建了一个HTML页面,作为运行测试的浏览器扩展的一部分。 HTML页面包括Jasmine库,扩展程序的JavaScript代码以及测试套件。测试将自动运行,并为您格式化结果。无需构建测试运行程序或结果格式化程序。只需遵循installation instructions,并使用此处记录的HTML创建测试运行器页面,并将测试套件也包含在页面中。
我认为您无法从另一台主机动态获取Jasmine框架,因此我只是在扩展程序中包含了Jasmine版本。当然,在构建生产扩展时,我将忽略它以及测试用例。
我还没有研究如何在命令行中执行测试。这对于自动部署工具将非常方便。
答案 6 :(得分:0)
要测试端到端,您可以使用 puppeteer
。
这是我为扩展程序编写的代码段,用于检查加载的扩展程序 title
并验证扩展程序是否在隐身模式下启用。
const path = require("path");
const puppeteer = require("puppeteer");
const assert = require("assert");
const Constants = require("../contants");
const Utils = require("./util");
const extensionID = Constants.EXTENSION_ID;
const extensionPath = path.join(__dirname, "../dist");
const extensionOptionHtml = "option.html";
const extPage = `chrome-extension://${extensionID}/${extensionOptionHtml}`;
let extensionPage = null;
let browser = null;
async function boot() {
browser = await puppeteer.launch({
// slowMo: 250,
headless: false, // extension are allowed only in head-full mode
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
"--no-sandbox",
"--disable-setuid-sandbox"
]
});
extensionPage = await browser.newPage();
await extensionPage.goto(extPage);
}
describe("Extension UI Testing", function() {
this.timeout(20000); // default is 2 seconds and that may not be enough to boot browsers and pages.
before(async function() {
await boot();
});
describe("option page home", async function() {
it("check title", async function() {
const h1 = "Allow extension in Incognito Mode";
const extH1 = await extensionPage.evaluate(() =>
document.querySelector("h1").textContent.trim()
);
assert.equal(extH1, h1);
});
it("show option ui after enabling extension in incognito", async () => {
await extensionPage.goto(`chrome://extensions/?id=${extensionID}`);
extensionPage.evaluate(() =>
document
.querySelector("body > extensions-manager")
.shadowRoot.querySelector("#viewManager > extensions-detail-view")
.shadowRoot.querySelector("#allow-incognito")
.shadowRoot.querySelector("#crToggle")
.click()
);
await Utils.sleep(2000);
await extensionPage.goto(
`chrome-extension://${extensionID}/${extensionOptionHtml}`
);
const h3 = "Mark Incognito";
const headingID = `#${Constants.OPTION_SCRIPT_HOST_ID} > div > div > header > div > h6`;
await extensionPage.waitFor(headingID);
console.log({ headingID });
const extH3 = await extensionPage.evaluate(headingID => {
return document.querySelector(headingID).textContent.trim();
}, headingID);
console.log({ extH3 });
assert.equal(extH3, h3);
});
});
after(async function() {
await browser.close();
});
});