Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Commit

Permalink
Bug 1433216 - Add AS Router targeting
Browse files Browse the repository at this point in the history
  • Loading branch information
k88hudson committed May 31, 2018
1 parent 74225d5 commit 52135de
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 18 deletions.
4 changes: 3 additions & 1 deletion karma.mc.config.js
Expand Up @@ -132,7 +132,9 @@ module.exports = function(config) {
include: [path.resolve("system-addon")],
exclude: [
path.resolve("system-addon/test/"),
path.resolve("system-addon/vendor")
path.resolve("system-addon/vendor"),
// This file is tested with mochi tests instead of mocha
path.resolve("system-addon/lib/ASRouterTargeting.jsm")
]
}
]
Expand Down
Expand Up @@ -23,6 +23,10 @@
"content": {
"type": "object",
"description": "An object containing all variables/props to be rendered in the template. See individual template schemas for details."
},
"targeting": {
"type": "string",
"description": "a JEXL expression representing targeting information"
}
},
"required": ["id", "template", "content"]
Expand Down
27 changes: 11 additions & 16 deletions system-addon/lib/ASRouter.jsm
Expand Up @@ -7,6 +7,9 @@ ChromeUtils.import("resource://gre/modules/Services.jsm");
Cu.importGlobalProperties(["fetch"]);
const {ASRouterActions: ra} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});

ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
"resource://activity-stream/lib/ASRouterTargeting.jsm");

const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
Expand Down Expand Up @@ -142,17 +145,6 @@ const MessageLoaderUtils = {

this.MessageLoaderUtils = MessageLoaderUtils;

/**
* getRandomItemFromArray
*
* @param {Array} arr An array of items
* @returns one of the items in the array
*/
function getRandomItemFromArray(arr) {
const index = Math.floor(Math.random() * arr.length);
return arr[index];
}

/**
* @class _ASRouter - Keeps track of all messages, UI surfaces, and
* handles blocking, rotation, etc. Inspecting ASRouter.state will
Expand Down Expand Up @@ -288,14 +280,17 @@ class _ASRouter {
return {bundle: bundledMessages, provider: originalMessage.provider, template: originalMessage.template};
}

_getUnblockedMessages() {
let {state} = this;
return state.messages.filter(item => !state.blockList.includes(item.id));
}

async sendNextMessage(target) {
let message;
let bundledMessages;
const msgs = this._getUnblockedMessages();
let message = await ASRouterTargeting.findMatchingMessage(msgs);
await this.setState({lastMessageId: message ? message.id : null});

await this.setState(state => {
message = getRandomItemFromArray(state.messages.filter(item => item.id !== state.lastMessageId && !state.blockList.includes(item.id)));
return {lastMessageId: message ? message.id : null};
});
// If this message needs to be bundled with other messages of the same template, find them and bundle them together
if (message && message.bundled) {
bundledMessages = this._getBundledMessages(message);
Expand Down
60 changes: 60 additions & 0 deletions system-addon/lib/ASRouterTargeting.jsm
@@ -0,0 +1,60 @@
ChromeUtils.import("resource://gre/modules/components-utils/FilterExpressions.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "ProfileAge",
"resource://gre/modules/ProfileAge.jsm");
ChromeUtils.import("resource://gre/modules/Console.jsm");

const FXA_USERNAME_PREF = "services.sync.username";

/**
* removeRandomItemFromArray - Removes a random item from the array and returns it.
*
* @param {Array} arr An array of items
* @returns one of the items in the array
*/
function removeRandomItemFromArray(arr) {
return arr.splice(Math.floor(Math.random() * arr.length), 1)[0];
}

const TargetingGetters = {
get profileAgeCreated() {
return new ProfileAge(null, null).created;
},
get profileAgeReset() {
return new ProfileAge(null, null).reset;
},
get hasFxAccount() {
return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);
}
};

this.ASRouterTargeting = {
Environment: TargetingGetters,

isMatch(filterExpression, context = this.Environment) {
return FilterExpressions.eval(filterExpression, context);
},

/**
* findMatchingMessage - Given an array of messages, returns one message
* whos targeting expression evaluates to true
*
* @param {Array} messages An array of AS router messages
* @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
* @returns {obj} an AS router message
*/
async findMatchingMessage(messages, context) {
const arrayOfItems = [...messages];
let match;
let candidate;
while (!match && arrayOfItems.length) {
candidate = removeRandomItemFromArray(arrayOfItems);
if (candidate && (!candidate.targeting || await this.isMatch(candidate.targeting, context))) {
match = candidate;
}
}
return match;
}
};

this.EXPORTED_SYMBOLS = ["ASRouterTargeting", "removeRandomItemFromArray"];
1 change: 1 addition & 0 deletions system-addon/test/functional/mochitest/browser.ini
Expand Up @@ -5,6 +5,7 @@ support-files =

[browser_as_load_location.js]
[browser_as_render.js]
[browser_asrouter_targeting.js]
[browser_getScreenshots.js]
[browser_highlights_section.js]
[browser_topsites_contextMenu_options.js]
Expand Down
@@ -0,0 +1,68 @@
ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
"resource://activity-stream/lib/ASRouterTargeting.jsm");
ChromeUtils.defineModuleGetter(this, "ProfileAge",
"resource://gre/modules/ProfileAge.jsm");

// ASRouterTargeting.isMatch
add_task(async function should_do_correct_targeting() {
is(await ASRouterTargeting.isMatch("FOO", {FOO: true}), true, "should return true for a matching value");
is(await ASRouterTargeting.isMatch("!FOO", {FOO: true}), false, "should return false for a non-matching value");
});

add_task(async function should_handle_async_getters() {
const context = {get FOO() { return Promise.resolve(true); }};
is(await ASRouterTargeting.isMatch("FOO", context), true, "should return true for a matching async value");
});

// ASRouterTargeting.findMatchingMessage
add_task(async function find_matching_message() {
const messages = [
{id: "foo", targeting: "FOO"},
{id: "bar", targeting: "!FOO"}
];
const context = {FOO: true};

const match = await ASRouterTargeting.findMatchingMessage(messages, context);

is(match, messages[0], "should match and return the correct message");
});

add_task(async function return_nothing_for_no_matching_message() {
const messages = [{id: "bar", targeting: "!FOO"}];
const context = {FOO: true};

const match = await ASRouterTargeting.findMatchingMessage(messages, context);

is(match, undefined, "should return nothing since no matching message exists");
});

// ASRouterTargeting.Environment
add_task(async function checkProfileAgeCreated() {
let profileAccessor = new ProfileAge();
is(await ASRouterTargeting.Environment.profileAgeCreated, await profileAccessor.created,
"should return correct profile age creation date");

const message = {id: "foo", targeting: `profileAgeCreated > ${await profileAccessor.created - 100}`};
is(await ASRouterTargeting.findMatchingMessage([message]), message,
"should select correct item by profile age created");
});

add_task(async function checkProfileAgeReset() {
let profileAccessor = new ProfileAge();
is(await ASRouterTargeting.Environment.profileAgeReset, await profileAccessor.reset,
"should return correct profile age reset");

const message = {id: "foo", targeting: `profileAgeReset == ${await profileAccessor.reset}`};
is(await ASRouterTargeting.findMatchingMessage([message]), message,
"should select correct item by profile age reset");
});

add_task(async function checkhasFxAccount() {
await pushPrefs(["services.sync.username", "someone@foo.com"]);
is(await ASRouterTargeting.Environment.hasFxAccount, true,
"should return true if a fx account is set");

const message = {id: "foo", targeting: "hasFxAccount"};
is(await ASRouterTargeting.findMatchingMessage([message]), message,
"should select correct item by hasFxAccount");
});
3 changes: 2 additions & 1 deletion system-addon/test/unit/unit-entry.js
Expand Up @@ -176,7 +176,8 @@ const TEST_GLOBAL = {
generateQI() { return {}; }
},
EventEmitter,
ShellService: {isDefaultBrowser: () => true}
ShellService: {isDefaultBrowser: () => true},
FilterExpressions: {eval() { return Promise.resolve(true); }}
};
overrider.set(TEST_GLOBAL);

Expand Down

0 comments on commit 52135de

Please sign in to comment.