380 lines
12 KiB
JavaScript
380 lines
12 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
FaviconProvider: "resource:///modules/topsites/TopSites.sys.mjs",
|
|
HttpServer: "resource://testing-common/httpd.sys.mjs",
|
|
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
|
|
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
|
|
PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
|
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
|
sinon: "resource://testing-common/Sinon.sys.mjs",
|
|
});
|
|
|
|
let TEST_FAVICON_FILE;
|
|
let TEST_DOMAIN;
|
|
let TEST_PAGE_URL;
|
|
let TEST_FAVICON_URL;
|
|
|
|
const TEST_SVG_DATA_URL = Services.io.newURI(
|
|
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy5" +
|
|
"3My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBmaWxs" +
|
|
"PSIjNDI0ZTVhIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iN" +
|
|
"DQiIHN0cm9rZT0iIzQyNGU1YSIgc3Ryb2tlLXdpZHRoPSIxMSIgZmlsbD" +
|
|
"0ibm9uZSIvPg0KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjI0LjYiIHI9IjY" +
|
|
"uNCIvPg0KICA8cmVjdCB4PSI0NSIgeT0iMzkuOSIgd2lkdGg9IjEwLjEi" +
|
|
"IGhlaWdodD0iNDEuOCIvPg0KPC9zdmc%2BDQo%3D"
|
|
);
|
|
|
|
add_setup(async function setup() {
|
|
info("Setup profile to use Places DB");
|
|
do_get_profile(true);
|
|
|
|
info("Setup favicon data");
|
|
TEST_FAVICON_FILE = do_get_file("favicon.png");
|
|
|
|
info("Setup http server that returns favicon content");
|
|
const httpServer = new HttpServer();
|
|
httpServer.registerPathHandler("/favicon.png", (request, response) => {
|
|
const inputStream = Cc[
|
|
"@mozilla.org/network/file-input-stream;1"
|
|
].createInstance(Ci.nsIFileInputStream);
|
|
inputStream.init(TEST_FAVICON_FILE, 0x01, -1, null);
|
|
const size = inputStream.available();
|
|
const faviconData = NetUtil.readInputStreamToString(inputStream, size);
|
|
|
|
response.setStatusLine(request.httpVersion, 200, "Ok");
|
|
response.setHeader("Content-Type", "image/png", false);
|
|
response.bodyOutputStream.write(faviconData, faviconData.length);
|
|
});
|
|
httpServer.start(-1);
|
|
|
|
TEST_DOMAIN = "localhost";
|
|
TEST_PAGE_URL = Services.io.newURI(
|
|
`http://${TEST_DOMAIN}:${httpServer.identity.primaryPort}`
|
|
);
|
|
TEST_FAVICON_URL = Services.io.newURI(
|
|
`http://${TEST_DOMAIN}:${httpServer.identity.primaryPort}/favicon.png`
|
|
);
|
|
|
|
info("Setup visit data in DB");
|
|
await PlacesTestUtils.addVisits(TEST_PAGE_URL);
|
|
|
|
// Save the original favicon service
|
|
let originalFaviconService = PlacesUtils.favicons;
|
|
|
|
registerCleanupFunction(async () => {
|
|
// Restore the original favicon service
|
|
PlacesUtils.favicons = originalFaviconService;
|
|
await new Promise(resolve => httpServer.stop(resolve));
|
|
await PlacesUtils.history.clear();
|
|
});
|
|
});
|
|
|
|
// Test for getFaviconDataURLFromNetwork() function.
|
|
add_task(async function test_getFaviconDataURLFromNetwork() {
|
|
const feed = new FaviconProvider();
|
|
|
|
info("Get favicon data via FaviconProvider");
|
|
const result = await feed.getFaviconDataURLFromNetwork(TEST_FAVICON_URL);
|
|
Assert.equal(
|
|
result.spec,
|
|
// eslint-disable-next-line no-use-before-define
|
|
await readFileDataAsDataURL(TEST_FAVICON_FILE, "image/png"),
|
|
"getFaviconDataURLFromNetwork returns correct data url"
|
|
);
|
|
});
|
|
|
|
// Test for fetchIcon() function. If there is valid favicon data for the page in
|
|
// DB, not update.
|
|
add_task(async function test_fetchIcon_with_valid_favicon() {
|
|
const feed = new FaviconProvider();
|
|
|
|
info("Setup stub to use dummy site data from FaviconProvider.getSite()");
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox
|
|
.stub(feed, "getSite")
|
|
.resolves({ domain: TEST_DOMAIN, image_url: TEST_FAVICON_URL.spec });
|
|
|
|
info("Setup valid favicon data in DB");
|
|
await PlacesUtils.favicons.setFaviconForPage(
|
|
TEST_PAGE_URL,
|
|
TEST_FAVICON_URL,
|
|
TEST_SVG_DATA_URL
|
|
);
|
|
|
|
info("Call FaviconProvider.fetchIcon()");
|
|
await feed.fetchIcon(TEST_PAGE_URL.spec);
|
|
|
|
info("Check the database");
|
|
const result = await PlacesUtils.favicons.getFaviconForPage(TEST_PAGE_URL);
|
|
Assert.equal(result.mimeType, "image/svg+xml");
|
|
Assert.equal(result.width, 65535);
|
|
|
|
info("Clean up");
|
|
await PlacesTestUtils.clearFavicons();
|
|
});
|
|
|
|
// Test for fetchIcon() function. If there is favicon data in DB but invalid or
|
|
// is not in DB, get favicon data from network, and update the DB with it.
|
|
add_task(async function test_fetchIcon_with_invalid_favicon() {
|
|
for (const dummyFaviconInfo of [
|
|
null,
|
|
{ iconUri: TEST_PAGE_URL, faviconSize: 0 },
|
|
]) {
|
|
info(`Test for ${dummyFaviconInfo}`);
|
|
const feed = new FaviconProvider();
|
|
|
|
info("Setup stub to use dummy site data from FaviconProvider.getSite()");
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox
|
|
.stub(feed, "getSite")
|
|
.resolves({ domain: TEST_DOMAIN, image_url: TEST_FAVICON_URL.spec });
|
|
|
|
info("Setup stub to simulate invalid favicon");
|
|
sandbox.stub(feed, "getFaviconInfo").resolves(dummyFaviconInfo);
|
|
|
|
info("Call FaviconProvider.fetchIcon()");
|
|
await feed.fetchIcon(TEST_PAGE_URL.spec);
|
|
|
|
info("Check the database");
|
|
const result = await PlacesUtils.favicons.getFaviconForPage(TEST_PAGE_URL);
|
|
// eslint-disable-next-line no-use-before-define
|
|
const expectedFaviconData = readFileData(TEST_FAVICON_FILE);
|
|
Assert.equal(result.uri.spec, `${TEST_FAVICON_URL.spec}#tippytop`);
|
|
Assert.deepEqual(result.rawData, expectedFaviconData);
|
|
Assert.equal(result.mimeType, "image/png");
|
|
Assert.equal(result.width, 16);
|
|
|
|
info("Clean up");
|
|
await PlacesTestUtils.clearFavicons();
|
|
sandbox.restore();
|
|
}
|
|
});
|
|
|
|
// Test for fetchIconFromRedirects() function. If there is valid favicon data
|
|
// for the redirected page in DB, copy the favicon data to the destination page as well.
|
|
add_task(async function test_fetchIconFromRedirects_with_valid_favicon() {
|
|
const feed = new FaviconProvider();
|
|
|
|
info("Setup stub to use dummy site data from FaviconProvider.getSite()");
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox
|
|
.stub(NewTabUtils.activityStreamProvider, "executePlacesQuery")
|
|
.resolves([
|
|
{ visit_id: 1, url: TEST_DOMAIN },
|
|
{ visit_id: 2, url: TEST_PAGE_URL.spec },
|
|
]);
|
|
|
|
info("Setup valid favicon data in DB");
|
|
await PlacesUtils.favicons.setFaviconForPage(
|
|
TEST_PAGE_URL,
|
|
TEST_FAVICON_URL,
|
|
TEST_SVG_DATA_URL
|
|
);
|
|
|
|
info("Setup destination");
|
|
const destination = Services.io.newURI("http://destination.localhost/");
|
|
await PlacesTestUtils.addVisits(destination);
|
|
|
|
info("Call FaviconProvider.fetchIconFromRedirects()");
|
|
await feed.fetchIconFromRedirects(destination.spec);
|
|
|
|
info("Check the database");
|
|
await TestUtils.waitForCondition(async () => {
|
|
const result = await PlacesUtils.favicons.getFaviconForPage(destination);
|
|
return !!result;
|
|
});
|
|
const sourceResult =
|
|
await PlacesUtils.favicons.getFaviconForPage(TEST_PAGE_URL);
|
|
const destinationResult =
|
|
await PlacesUtils.favicons.getFaviconForPage(destination);
|
|
Assert.deepEqual(destinationResult.rawData, sourceResult.rawData);
|
|
Assert.equal(destinationResult.mimeType, sourceResult.mimeType);
|
|
Assert.equal(destinationResult.width, sourceResult.width);
|
|
|
|
info("Clean up");
|
|
await PlacesTestUtils.clearFavicons();
|
|
sandbox.restore();
|
|
});
|
|
|
|
// Copy from toolkit/components/places/tests/head_common.js
|
|
function readInputStreamData(aStream) {
|
|
let bistream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
|
|
Ci.nsIBinaryInputStream
|
|
);
|
|
try {
|
|
bistream.setInputStream(aStream);
|
|
let expectedData = [];
|
|
let avail;
|
|
while ((avail = bistream.available())) {
|
|
expectedData = expectedData.concat(bistream.readByteArray(avail));
|
|
}
|
|
return expectedData;
|
|
} finally {
|
|
bistream.close();
|
|
}
|
|
}
|
|
|
|
function readFileData(aFile) {
|
|
let inputStream = Cc[
|
|
"@mozilla.org/network/file-input-stream;1"
|
|
].createInstance(Ci.nsIFileInputStream);
|
|
// init the stream as RD_ONLY, -1 == default permissions.
|
|
inputStream.init(aFile, 0x01, -1, null);
|
|
|
|
// Check the returned size versus the expected size.
|
|
let size = inputStream.available();
|
|
let bytes = readInputStreamData(inputStream);
|
|
if (size !== bytes.length) {
|
|
throw new Error("Didn't read expected number of bytes");
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
async function fileDataToDataURL(data, mimeType) {
|
|
const dataURL = await new Promise(resolve => {
|
|
const buffer = new Uint8ClampedArray(data);
|
|
const blob = new Blob([buffer], { type: mimeType });
|
|
const reader = new FileReader();
|
|
reader.onload = e => {
|
|
resolve(e.target.result);
|
|
};
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
return dataURL;
|
|
}
|
|
|
|
async function readFileDataAsDataURL(file, mimeType) {
|
|
const data = readFileData(file);
|
|
return fileDataToDataURL(data, mimeType);
|
|
}
|
|
|
|
const FAKE_SMALLPNG_DATA_URI =
|
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
|
|
|
|
// Test: fetchIcon should fetch favicon from network if no data in DB
|
|
add_task(async function test_fetchIcon_withNetworkFetch() {
|
|
const sandbox = sinon.createSandbox();
|
|
let feed = new FaviconProvider();
|
|
let url = "https://mozilla.org/";
|
|
|
|
// Set up mocks
|
|
PlacesUtils.favicons = {
|
|
getFaviconForPage: sandbox.stub().returns(Promise.resolve(null)),
|
|
setFaviconForPage: sandbox.spy(),
|
|
copyFavicons: sandbox.spy(),
|
|
};
|
|
feed.getSite = sandbox
|
|
.stub()
|
|
.returns(
|
|
Promise.resolve({ domain: "mozilla.org", image_url: `${url}/icon.png` })
|
|
);
|
|
|
|
// Mock the getFaviconDataURLFromNetwork method
|
|
sandbox
|
|
.stub(feed, "getFaviconDataURLFromNetwork")
|
|
.resolves({ spec: FAKE_SMALLPNG_DATA_URI });
|
|
|
|
await feed.fetchIcon(url);
|
|
|
|
// Assertions
|
|
Assert.equal(PlacesUtils.favicons.setFaviconForPage.calledOnce, true);
|
|
Assert.equal(
|
|
PlacesUtils.favicons.setFaviconForPage.firstCall.args[2].spec,
|
|
FAKE_SMALLPNG_DATA_URI
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
// Test: fetchIcon should fetch favicon from network if invalid data in DB
|
|
add_task(async function test_fetchIcon_withInvalidDataInDb() {
|
|
const sandbox = sinon.createSandbox();
|
|
// Set up mocks
|
|
PlacesUtils.favicons = {
|
|
// Invalid since no width.
|
|
getFaviconForPage: sandbox
|
|
.stub()
|
|
.returns(Promise.resolve({ iconUri: { spec: FAKE_SMALLPNG_DATA_URI } })),
|
|
setFaviconForPage: sandbox.spy(),
|
|
copyFavicons: sandbox.spy(),
|
|
};
|
|
|
|
let feed = new FaviconProvider();
|
|
let url = "https://mozilla.org/";
|
|
feed.getSite = sandbox
|
|
.stub()
|
|
.returns(
|
|
Promise.resolve({ domain: "mozilla.org", image_url: `${url}/icon.png` })
|
|
);
|
|
|
|
// Mock the getFaviconDataURLFromNetwork method
|
|
sandbox
|
|
.stub(feed, "getFaviconDataURLFromNetwork")
|
|
.resolves({ spec: FAKE_SMALLPNG_DATA_URI });
|
|
|
|
await feed.fetchIcon(url);
|
|
|
|
// Assertions
|
|
Assert.equal(PlacesUtils.favicons.setFaviconForPage.calledOnce, true);
|
|
Assert.equal(
|
|
PlacesUtils.favicons.setFaviconForPage.firstCall.args[2].spec,
|
|
FAKE_SMALLPNG_DATA_URI
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
// Test: fetchIcon should not set favicon if valid data exists in DB
|
|
add_task(async function test_fetchIcon_withValidDataInDb() {
|
|
const sandbox = sinon.createSandbox();
|
|
// Set up mocks
|
|
PlacesUtils.favicons = {
|
|
getFaviconForPage: sandbox.stub().returns(
|
|
Promise.resolve({
|
|
iconUri: { spec: FAKE_SMALLPNG_DATA_URI },
|
|
width: 100,
|
|
})
|
|
),
|
|
setFaviconForPage: sandbox.spy(),
|
|
copyFavicons: sandbox.spy(),
|
|
};
|
|
let feed = new FaviconProvider();
|
|
let url = "https://mozilla.org/";
|
|
feed.getSite = sandbox
|
|
.stub()
|
|
.returns(
|
|
Promise.resolve({ domain: "mozilla.org", image_url: `${url}/icon.png` })
|
|
);
|
|
|
|
await feed.fetchIcon(url);
|
|
|
|
// Assertions
|
|
Assert.equal(PlacesUtils.favicons.setFaviconForPage.called, false);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
// Test: fetchIcon should not set favicon if the URL is not in TippyTop data
|
|
add_task(async function test_fetchIcon_withNoTippyTopData() {
|
|
const sandbox = sinon.createSandbox();
|
|
let feed = new FaviconProvider();
|
|
// Set up mocks
|
|
PlacesUtils.favicons = {
|
|
getFaviconForPage: sandbox.stub().returns(Promise.resolve(null)),
|
|
setFaviconForPage: sandbox.spy(),
|
|
copyFavicons: sandbox.spy(),
|
|
};
|
|
feed.getSite = sandbox.stub().returns(Promise.resolve(null));
|
|
|
|
await feed.fetchIcon("https://example.com");
|
|
|
|
// Assertions
|
|
Assert.equal(PlacesUtils.favicons.setFaviconForPage.called, false);
|
|
|
|
sandbox.restore();
|
|
});
|