summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js')
-rw-r--r--browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js456
1 files changed, 456 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js b/browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js
new file mode 100644
index 0000000000..6dd483ae70
--- /dev/null
+++ b/browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js
@@ -0,0 +1,456 @@
+import { GlobalOverrider } from "test/unit/utils";
+import { PersonalityProviderWorker } from "lib/PersonalityProvider/PersonalityProviderWorkerClass.jsm";
+import {
+ tokenize,
+ toksToTfIdfVector,
+} from "lib/PersonalityProvider/Tokenize.jsm";
+import { RecipeExecutor } from "lib/PersonalityProvider/RecipeExecutor.jsm";
+import { NmfTextTagger } from "lib/PersonalityProvider/NmfTextTagger.jsm";
+import { NaiveBayesTextTagger } from "lib/PersonalityProvider/NaiveBayesTextTagger.jsm";
+
+describe("Personality Provider Worker Class", () => {
+ let instance;
+ let globals;
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ globals = new GlobalOverrider();
+ globals.set("tokenize", tokenize);
+ globals.set("toksToTfIdfVector", toksToTfIdfVector);
+ globals.set("NaiveBayesTextTagger", NaiveBayesTextTagger);
+ globals.set("NmfTextTagger", NmfTextTagger);
+ globals.set("RecipeExecutor", RecipeExecutor);
+ instance = new PersonalityProviderWorker();
+
+ // mock the RecipeExecutor
+ instance.recipeExecutor = {
+ executeRecipe: (item, recipe) => {
+ if (recipe === "history_item_builder") {
+ if (item.title === "fail") {
+ return null;
+ }
+ return {
+ title: item.title,
+ score: item.frecency,
+ type: "history_item",
+ };
+ } else if (recipe === "interest_finalizer") {
+ return {
+ title: item.title,
+ score: item.score * 100,
+ type: "interest_vector",
+ };
+ } else if (recipe === "item_to_rank_builder") {
+ if (item.title === "fail") {
+ return null;
+ }
+ return {
+ item_title: item.title,
+ item_score: item.score,
+ type: "item_to_rank",
+ };
+ } else if (recipe === "item_ranker") {
+ if (item.title === "fail" || item.item_title === "fail") {
+ return null;
+ }
+ return {
+ title: item.title,
+ score: item.item_score * item.score,
+ type: "ranked_item",
+ };
+ }
+ return null;
+ },
+ executeCombinerRecipe: (item1, item2, recipe) => {
+ if (recipe === "interest_combiner") {
+ if (
+ item1.title === "combiner_fail" ||
+ item2.title === "combiner_fail"
+ ) {
+ return null;
+ }
+ if (item1.type === undefined) {
+ item1.type = "combined_iv";
+ }
+ if (item1.score === undefined) {
+ item1.score = 0;
+ }
+ return { type: item1.type, score: item1.score + item2.score };
+ }
+ return null;
+ },
+ };
+
+ instance.interestConfig = {
+ history_item_builder: "history_item_builder",
+ history_required_fields: ["a", "b", "c"],
+ interest_finalizer: "interest_finalizer",
+ item_to_rank_builder: "item_to_rank_builder",
+ item_ranker: "item_ranker",
+ interest_combiner: "interest_combiner",
+ };
+ });
+ afterEach(() => {
+ sinon.restore();
+ sandbox.restore();
+ globals.restore();
+ });
+ describe("#setBaseAttachmentsURL", () => {
+ it("should set baseAttachmentsURL", () => {
+ instance.setBaseAttachmentsURL("url");
+ assert.equal(instance.baseAttachmentsURL, "url");
+ });
+ });
+ describe("#setInterestConfig", () => {
+ it("should set interestConfig", () => {
+ instance.setInterestConfig("config");
+ assert.equal(instance.interestConfig, "config");
+ });
+ });
+ describe("#setInterestVector", () => {
+ it("should set interestVector", () => {
+ instance.setInterestVector("vector");
+ assert.equal(instance.interestVector, "vector");
+ });
+ });
+ describe("#onSync", async () => {
+ it("should sync remote settings collection from onSync", async () => {
+ sinon.stub(instance, "deleteAttachment").resolves();
+ sinon.stub(instance, "maybeDownloadAttachment").resolves();
+
+ instance.onSync({
+ data: {
+ created: ["create-1", "create-2"],
+ updated: [
+ { old: "update-old-1", new: "update-new-1" },
+ { old: "update-old-2", new: "update-new-2" },
+ ],
+ deleted: ["delete-2", "delete-1"],
+ },
+ });
+
+ assert(instance.maybeDownloadAttachment.withArgs("create-1").calledOnce);
+ assert(instance.maybeDownloadAttachment.withArgs("create-2").calledOnce);
+ assert(
+ instance.maybeDownloadAttachment.withArgs("update-new-1").calledOnce
+ );
+ assert(
+ instance.maybeDownloadAttachment.withArgs("update-new-2").calledOnce
+ );
+
+ assert(instance.deleteAttachment.withArgs("delete-1").calledOnce);
+ assert(instance.deleteAttachment.withArgs("delete-2").calledOnce);
+ assert(instance.deleteAttachment.withArgs("update-old-1").calledOnce);
+ assert(instance.deleteAttachment.withArgs("update-old-2").calledOnce);
+ });
+ });
+ describe("#maybeDownloadAttachment", () => {
+ it("should attempt _downloadAttachment three times for maybeDownloadAttachment", async () => {
+ let existsStub;
+ let statStub;
+ let attachmentStub;
+ sinon.stub(instance, "_downloadAttachment").resolves();
+ const makeDirStub = globals.sandbox
+ .stub(global.IOUtils, "makeDirectory")
+ .resolves();
+
+ existsStub = globals.sandbox
+ .stub(global.IOUtils, "exists")
+ .resolves(true);
+
+ statStub = globals.sandbox
+ .stub(global.IOUtils, "stat")
+ .resolves({ size: "1" });
+
+ attachmentStub = {
+ attachment: {
+ filename: "file",
+ size: "1",
+ // This hash matches the hash generated from the empty Uint8Array returned by the IOUtils.read stub.
+ hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ },
+ };
+
+ await instance.maybeDownloadAttachment(attachmentStub);
+ assert.calledWith(makeDirStub, "personality-provider");
+ assert.calledOnce(existsStub);
+ assert.calledOnce(statStub);
+ assert.notCalled(instance._downloadAttachment);
+
+ existsStub.resetHistory();
+ statStub.resetHistory();
+ instance._downloadAttachment.resetHistory();
+
+ attachmentStub = {
+ attachment: {
+ filename: "file",
+ size: "2",
+ },
+ };
+
+ await instance.maybeDownloadAttachment(attachmentStub);
+ assert.calledThrice(existsStub);
+ assert.calledThrice(statStub);
+ assert.calledThrice(instance._downloadAttachment);
+
+ existsStub.resetHistory();
+ statStub.resetHistory();
+ instance._downloadAttachment.resetHistory();
+
+ attachmentStub = {
+ attachment: {
+ filename: "file",
+ size: "1",
+ // Bogus hash to trigger an update.
+ hash: "1234",
+ },
+ };
+
+ await instance.maybeDownloadAttachment(attachmentStub);
+ assert.calledThrice(existsStub);
+ assert.calledThrice(statStub);
+ assert.calledThrice(instance._downloadAttachment);
+ });
+ });
+ describe("#_downloadAttachment", () => {
+ beforeEach(() => {
+ globals.set("Uint8Array", class Uint8Array {});
+ });
+ it("should write a file from _downloadAttachment", async () => {
+ globals.set(
+ "XMLHttpRequest",
+ class {
+ constructor() {
+ this.status = 200;
+ this.response = "response!";
+ }
+ open() {}
+ setRequestHeader() {}
+ send() {}
+ }
+ );
+
+ const ioutilsWriteStub = globals.sandbox
+ .stub(global.IOUtils, "write")
+ .resolves();
+
+ await instance._downloadAttachment({
+ attachment: { location: "location", filename: "filename" },
+ });
+
+ const writeArgs = ioutilsWriteStub.firstCall.args;
+ assert.equal(writeArgs[0], "filename");
+ assert.equal(writeArgs[2].tmpPath, "filename.tmp");
+ });
+ it("should call console.error from _downloadAttachment if not valid response", async () => {
+ globals.set(
+ "XMLHttpRequest",
+ class {
+ constructor() {
+ this.status = 0;
+ this.response = "response!";
+ }
+ open() {}
+ setRequestHeader() {}
+ send() {}
+ }
+ );
+
+ const consoleErrorStub = globals.sandbox
+ .stub(console, "error")
+ .resolves();
+
+ await instance._downloadAttachment({
+ attachment: { location: "location", filename: "filename" },
+ });
+
+ assert.calledOnce(consoleErrorStub);
+ });
+ });
+ describe("#deleteAttachment", () => {
+ it("should remove attachments when calling deleteAttachment", async () => {
+ const makeDirStub = globals.sandbox
+ .stub(global.IOUtils, "makeDirectory")
+ .resolves();
+ const removeStub = globals.sandbox
+ .stub(global.IOUtils, "remove")
+ .resolves();
+ await instance.deleteAttachment({ attachment: { filename: "filename" } });
+ assert.calledOnce(makeDirStub);
+ assert.calledTwice(removeStub);
+ assert.calledWith(removeStub.firstCall, "filename", {
+ ignoreAbsent: true,
+ });
+ assert.calledWith(removeStub.secondCall, "personality-provider", {
+ ignoreAbsent: true,
+ });
+ });
+ });
+ describe("#getAttachment", () => {
+ it("should return JSON when calling getAttachment", async () => {
+ sinon.stub(instance, "maybeDownloadAttachment").resolves();
+ const readJSONStub = globals.sandbox
+ .stub(global.IOUtils, "readJSON")
+ .resolves({});
+ const record = { attachment: { filename: "filename" } };
+ let returnValue = await instance.getAttachment(record);
+
+ assert.calledOnce(readJSONStub);
+ assert.calledWith(readJSONStub, "filename");
+ assert.calledOnce(instance.maybeDownloadAttachment);
+ assert.calledWith(instance.maybeDownloadAttachment, record);
+ assert.deepEqual(returnValue, {});
+
+ readJSONStub.restore();
+ globals.sandbox.stub(global.IOUtils, "readJSON").throws("foo");
+ const consoleErrorStub = globals.sandbox
+ .stub(console, "error")
+ .resolves();
+ returnValue = await instance.getAttachment(record);
+
+ assert.calledOnce(consoleErrorStub);
+ assert.deepEqual(returnValue, {});
+ });
+ });
+ describe("#fetchModels", () => {
+ it("should return ok true", async () => {
+ sinon.stub(instance, "getAttachment").resolves();
+ const result = await instance.fetchModels([{ key: 1234 }]);
+ assert.isTrue(result.ok);
+ assert.deepEqual(instance.models, [{ recordKey: 1234 }]);
+ });
+ it("should return ok false", async () => {
+ sinon.stub(instance, "getAttachment").resolves();
+ const result = await instance.fetchModels([]);
+ assert.isTrue(!result.ok);
+ });
+ });
+ describe("#generateTaggers", () => {
+ it("should generate taggers from modelKeys", () => {
+ const modelKeys = ["nb_model_sports", "nmf_model_sports"];
+
+ instance.models = [
+ { recordKey: "nb_model_sports", model_type: "nb" },
+ {
+ recordKey: "nmf_model_sports",
+ model_type: "nmf",
+ parent_tag: "nmf_sports_parent_tag",
+ },
+ ];
+
+ instance.generateTaggers(modelKeys);
+ assert.equal(instance.taggers.nbTaggers.length, 1);
+ assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 1);
+ });
+ it("should skip any models not in modelKeys", () => {
+ const modelKeys = ["nb_model_sports"];
+
+ instance.models = [
+ { recordKey: "nb_model_sports", model_type: "nb" },
+ {
+ recordKey: "nmf_model_sports",
+ model_type: "nmf",
+ parent_tag: "nmf_sports_parent_tag",
+ },
+ ];
+
+ instance.generateTaggers(modelKeys);
+ assert.equal(instance.taggers.nbTaggers.length, 1);
+ assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 0);
+ });
+ it("should skip any models not defined", () => {
+ const modelKeys = ["nb_model_sports", "nmf_model_sports"];
+
+ instance.models = [{ recordKey: "nb_model_sports", model_type: "nb" }];
+ instance.generateTaggers(modelKeys);
+ assert.equal(instance.taggers.nbTaggers.length, 1);
+ assert.equal(Object.keys(instance.taggers.nmfTaggers).length, 0);
+ });
+ });
+ describe("#generateRecipeExecutor", () => {
+ it("should generate a recipeExecutor", () => {
+ instance.recipeExecutor = null;
+ instance.taggers = {};
+ instance.generateRecipeExecutor();
+ assert.isNotNull(instance.recipeExecutor);
+ });
+ });
+ describe("#createInterestVector", () => {
+ let mockHistory = [];
+ beforeEach(() => {
+ mockHistory = [
+ {
+ title: "automotive",
+ description: "something about automotive",
+ url: "http://example.com/automotive",
+ frecency: 10,
+ },
+ {
+ title: "fashion",
+ description: "something about fashion",
+ url: "http://example.com/fashion",
+ frecency: 5,
+ },
+ {
+ title: "tech",
+ description: "something about tech",
+ url: "http://example.com/tech",
+ frecency: 1,
+ },
+ ];
+ });
+ it("should gracefully handle history entries that fail", () => {
+ mockHistory.push({ title: "fail" });
+ assert.isNotNull(instance.createInterestVector(mockHistory));
+ });
+
+ it("should fail if the combiner fails", () => {
+ mockHistory.push({ title: "combiner_fail", frecency: 111 });
+ let actual = instance.createInterestVector(mockHistory);
+ assert.isNull(actual);
+ });
+
+ it("should process history, combine, and finalize", () => {
+ let actual = instance.createInterestVector(mockHistory);
+ assert.equal(actual.interestVector.score, 1600);
+ });
+ });
+ describe("#calculateItemRelevanceScore", () => {
+ it("should return null for busted item", () => {
+ assert.equal(
+ instance.calculateItemRelevanceScore({ title: "fail" }),
+ null
+ );
+ });
+ it("should return null for a busted ranking", () => {
+ instance.interestVector = { title: "fail", score: 10 };
+ assert.equal(
+ instance.calculateItemRelevanceScore({ title: "some item", score: 6 }),
+ null
+ );
+ });
+ it("should return a score, and not change with interestVector", () => {
+ instance.interestVector = { score: 10 };
+ assert.equal(
+ instance.calculateItemRelevanceScore({ score: 2 }).rankingVector.score,
+ 20
+ );
+ assert.deepEqual(instance.interestVector, { score: 10 });
+ });
+ it("should use defined personalization_models if available", () => {
+ instance.interestVector = { score: 10 };
+ const item = {
+ score: 2,
+ personalization_models: {
+ entertainment: 1,
+ },
+ };
+ assert.equal(
+ instance.calculateItemRelevanceScore(item).scorableItem.item_tags
+ .entertainment,
+ 1
+ );
+ });
+ });
+});