Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test runs only with pattern: os != 'android'
- Manifest: browser/extensions/newtab/test/xpcshell/xpcshell.toml
/* Any copyright is dedicated to the Public Domain.
"use strict";
ChromeUtils.defineESModuleGetters(this, {
RemoteRenderer: "resource://newtab/lib/RemoteRenderer.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
});
const TEST_BUNDLED_VERSION = "1.0.0";
const TEST_VERSION = "1.0.1";
const TEST_MANIFEST = JSON.stringify({
version: TEST_VERSION,
buildTime: "2026-03-10T00:00:00.000Z",
file: "index.abc123.js",
hash: "abc123",
dataSchemaVersion: "1.2.1",
cssFile: "renderer.def456.css",
});
const TEST_JS = "console.log('test js');";
const TEST_CSS = "body { color: red; }";
/**
* Converts a string into an ArrayBuffer that can be streamed as an
* nsIInputStream.
*
* @param {string} str
* @returns {ArrayBuffer}
*/
function stringToArrayBuffer(str) {
let encoder = new TextEncoder();
return encoder.encode(str).buffer;
}
/**
* Utility function to poke a fake renderer version into prefs to pretend like
* it's the current available one. Does not actually write anything into the
* cache.
*
* @param {string} version
*/
function setCachedVersionPref(version) {
Services.prefs.setCharPref(
"browser.newtabpage.activity-stream.remote-renderer.version",
version
);
}
add_task(async function test_updateAndReadCache() {
info("RemoteRenderer should write and read manifest, JS, and CSS streams");
let renderer = new RemoteRenderer();
await renderer.updateFromRemoteSettings({
manifest: stringToArrayBuffer(TEST_MANIFEST),
js: stringToArrayBuffer(TEST_JS),
css: stringToArrayBuffer(TEST_CSS),
version: TEST_VERSION,
});
let manifestURI = renderer.makeManifestEntryURI(TEST_VERSION);
let scriptURI = renderer.makeScriptEntryURI(TEST_VERSION);
let styleURI = renderer.makeStyleEntryURI(TEST_VERSION);
let [manifestEntry, scriptEntry, styleEntry] = await Promise.all([
renderer.openCacheEntry(manifestURI),
renderer.openCacheEntry(scriptURI),
renderer.openCacheEntry(styleURI),
]);
Assert.ok(manifestEntry, "Should have manifest entry");
Assert.ok(scriptEntry, "Should have script entry");
Assert.ok(styleEntry, "Should have style entry");
Assert.equal(
manifestEntry.getMetaDataElement("version"),
TEST_VERSION,
"Manifest version should match"
);
Assert.equal(
scriptEntry.getMetaDataElement("version"),
TEST_VERSION,
"Script version should match"
);
Assert.equal(
styleEntry.getMetaDataElement("version"),
TEST_VERSION,
"Style version should match"
);
let manifestStream = manifestEntry.openInputStream(0);
let manifestData = await renderer.pumpInputStreamToString(manifestStream);
Assert.equal(manifestData, TEST_MANIFEST, "Manifest content should match");
let scriptStream = scriptEntry.openInputStream(0);
let scriptData = await renderer.pumpInputStreamToString(scriptStream);
Assert.equal(scriptData, TEST_JS, "JS content should match");
let styleStream = styleEntry.openInputStream(0);
let styleData = await renderer.pumpInputStreamToString(styleStream);
Assert.equal(styleData, TEST_CSS, "CSS content should match");
await renderer.resetCache();
});
add_task(async function test_incompleteCache() {
info(
"RemoteRenderer should handle cache with missing entries (not all three files present)"
);
let renderer = new RemoteRenderer();
Services.prefs.clearUserPref(
"browser.newtabpage.activity-stream.remote-renderer.version"
);
let manifestURI = renderer.makeManifestEntryURI(TEST_VERSION);
let scriptURI = renderer.makeScriptEntryURI(TEST_VERSION);
let styleURI = renderer.makeStyleEntryURI(TEST_VERSION);
await Promise.all([
renderer.doomCacheEntryOnShutdown(manifestURI),
renderer.doomCacheEntryOnShutdown(scriptURI),
renderer.doomCacheEntryOnShutdown(styleURI),
]);
// Pretend that we've shutdown, to destroy these entries.
renderer.onShutdown();
await renderer.writeCacheEntry(
scriptURI,
stringToArrayBuffer(TEST_JS),
TEST_VERSION
);
Services.prefs.setCharPref(
"browser.newtabpage.activity-stream.remote-renderer.version",
TEST_VERSION
);
let result = await renderer.assign();
Assert.ok(
!result.appProps.isCached,
"Should fall back to bundled version when cache is incomplete"
);
await renderer.resetCache();
});
add_task(async function test_resetCache() {
info(
"RemoteRenderer.resetCache should clear prefs and scheduled cache entries to be doomed on shutdown"
);
let sandbox = sinon.createSandbox();
let renderer = new RemoteRenderer();
sandbox
.stub(renderer.constructor, "BUNDLED_VERSION")
.get(() => TEST_BUNDLED_VERSION);
await renderer.updateFromRemoteSettings({
manifest: stringToArrayBuffer(TEST_MANIFEST),
js: stringToArrayBuffer(TEST_JS),
css: stringToArrayBuffer(TEST_CSS),
version: TEST_VERSION,
});
let versionPref = Services.prefs.getCharPref(
"browser.newtabpage.activity-stream.remote-renderer.version",
""
);
Assert.equal(versionPref, TEST_VERSION, "Version pref should be set");
let resultBefore = await renderer.assign();
Assert.ok(
resultBefore.appProps.isCached,
"Should use cached version before reset"
);
let manifestURI = renderer.makeManifestEntryURI(TEST_VERSION);
let scriptURI = renderer.makeScriptEntryURI(TEST_VERSION);
let styleURI = renderer.makeStyleEntryURI(TEST_VERSION);
Assert.ok(
!renderer.willDoomOnShutdown(manifestURI),
"Not yet scheduled to doom the manifest entry"
);
Assert.ok(
!renderer.willDoomOnShutdown(scriptURI),
"Not yet scheduled to doom the script entry"
);
Assert.ok(
!renderer.willDoomOnShutdown(styleURI),
"Not yet scheduled to doom the style entry"
);
await renderer.resetCache();
versionPref = Services.prefs.getCharPref(
"browser.newtabpage.activity-stream.remote-renderer.version",
""
);
Assert.equal(versionPref, "", "Version pref should be cleared after reset");
Assert.ok(
renderer.willDoomOnShutdown(manifestURI),
"Scheduled to doom the manifest entry"
);
Assert.ok(
renderer.willDoomOnShutdown(scriptURI),
"Scheduled to doom the script entry"
);
Assert.ok(
renderer.willDoomOnShutdown(styleURI),
"Scheduled to doom the style entry"
);
let resultAfter = await renderer.assign();
Assert.ok(
!resultAfter.appProps.isCached,
"Should fall back to bundled version after reset"
);
sandbox.restore();
});
add_task(async function test_emptyCacheBehavior() {
info(
"RemoteRenderer should fall back to bundled version when cache is empty"
);
let renderer = new RemoteRenderer();
await renderer.resetCache();
let result = await renderer.assign();
Assert.ok(result, "Should return result");
Assert.ok(result.appProps, "Should have appProps");
Assert.equal(
result.appProps.isCached,
false,
"Should not be using cached version"
);
Assert.ok(result.appProps.manifest, "Should have a manifest");
});
add_task(async function test_metadataTimestamp() {
info("RemoteRenderer should store timestamp metadata");
let renderer = new RemoteRenderer();
let beforeWrite = Date.now();
await renderer.updateFromRemoteSettings({
manifest: stringToArrayBuffer(TEST_MANIFEST),
js: stringToArrayBuffer(TEST_JS),
css: stringToArrayBuffer(TEST_CSS),
version: TEST_VERSION,
});
let afterWrite = Date.now();
let scriptURI = renderer.makeScriptEntryURI(TEST_VERSION);
let scriptEntry = await renderer.openCacheEntry(scriptURI);
Assert.ok(scriptEntry, "Cache entry should exist");
let timestamp = parseInt(scriptEntry.getMetaDataElement("timestamp"), 10);
Assert.ok(
timestamp >= beforeWrite && timestamp <= afterWrite,
"Timestamp should be within write window"
);
await renderer.resetCache();
});
add_task(async function test_streamReusability() {
info("Cache entries should allow opening multiple input streams");
let renderer = new RemoteRenderer();
await renderer.updateFromRemoteSettings({
manifest: stringToArrayBuffer(TEST_MANIFEST),
js: stringToArrayBuffer(TEST_JS),
css: stringToArrayBuffer(TEST_CSS),
version: TEST_VERSION,
});
let scriptURI = renderer.makeScriptEntryURI(TEST_VERSION);
let scriptEntry = await renderer.openCacheEntry(scriptURI);
Assert.ok(scriptEntry, "Cache entry should exist");
let firstStream = scriptEntry.openInputStream(0);
let firstRead = await renderer.pumpInputStreamToString(firstStream);
Assert.equal(firstRead, TEST_JS, "First read should match");
let secondStream = scriptEntry.openInputStream(0);
let secondRead = await renderer.pumpInputStreamToString(secondStream);
Assert.equal(secondRead, TEST_JS, "Second read should match");
await renderer.resetCache();
});
add_task(async function test_versionedURIs() {
info("RemoteRenderer should use version-based cache URIs");
let renderer = new RemoteRenderer();
let version1 = "1.0.0";
let version2 = "2.0.0";
let manifestURI1 = renderer.makeManifestEntryURI(version1);
let manifestURI2 = renderer.makeManifestEntryURI(version2);
Assert.notEqual(
manifestURI1.spec,
manifestURI2.spec,
"Different versions should produce different URIs"
);
Assert.ok(manifestURI1.spec.includes(version1), "URI should contain version");
let scriptURI1 = renderer.makeScriptEntryURI(version1);
let scriptURI2 = renderer.makeScriptEntryURI(version2);
Assert.notEqual(
scriptURI1.spec,
scriptURI2.spec,
"Different versions should produce different script URIs"
);
let styleURI1 = renderer.makeStyleEntryURI(version1);
let styleURI2 = renderer.makeStyleEntryURI(version2);
Assert.notEqual(
styleURI1.spec,
styleURI2.spec,
"Different versions should produce different style URIs"
);
});
add_task(async function test_shutdownObserver() {
info("RemoteRenderer should observer the quit topic");
let sandbox = sinon.createSandbox();
let renderer = new RemoteRenderer();
sandbox.stub(renderer, "onShutdown");
Services.obs.notifyObservers(null, "quit-application-granted");
Assert.ok(
renderer.onShutdown.calledOnce,
"quit-application-granted caused onShutdown to be called"
);
sandbox.restore();
});
add_task(async function test_higher_bundled_version() {
info(
"RemoteRenderer should prefer the bundled version when its version number is higher than the cache."
);
let sandbox = sinon.createSandbox();
let renderer = new RemoteRenderer();
sandbox.stub(RemoteRenderer, "BUNDLED_VERSION").get(() => "1.0.1");
setCachedVersionPref("1.0.0");
sandbox.stub(renderer, "makeManifestEntryURI");
sandbox.stub(renderer, "makeScriptEntryURI");
sandbox.stub(renderer, "makeStyleEntryURI");
sandbox.spy(renderer, "resetCache");
let result = await renderer.assign();
Assert.ok(
!result.appProps.isCached,
"Should use the bundled version when cache has a lower version number"
);
Assert.ok(
renderer.resetCache.calledOnce,
"We should have cleared the cache once we realized the bundled renderer " +
"had a higher version"
);
sandbox.restore();
});
add_task(async function test_revalidate_on_calling_assign() {
info("RemoteRenderer should attempt to revalidate after calling assign.");
let sandbox = sinon.createSandbox();
// We override this first because this static getter is read on
// RemoteRenderer construction to set up the DeferredTask delay.
sandbox.stub(RemoteRenderer, "REVALIDATION_DEBOUNCE_RATE_MS").get(() => 0);
let renderer = new RemoteRenderer();
sandbox.stub(renderer, "maybeRevalidate");
await renderer.assign();
await TestUtils.waitForCondition(() => {
return renderer.maybeRevalidate.calledOnce;
}, "Should call maybeRevalidate after assign");
sandbox.restore();
});