Merge pull request #32 from ngokevin/sync

upstream and resync fireplace to commonplace (bug 999703)
This commit is contained in:
Kevin Ngo 2014-05-08 17:34:48 -07:00
Родитель 3729b22ba2 c4f73aaef0
Коммит cddfeef4d6
22 изменённых файлов: 971 добавлений и 116 удалений

116
src/media/js/buckets.js Normal file
Просмотреть файл

@ -0,0 +1,116 @@
define('buckets', [], function() {
function noop() {return '';}
var aelem = document.createElement('audio');
var velem = document.createElement('video');
// Compatibilty with PhantomJS, which doesn't implement canPlayType
if (!('canPlayType' in aelem)) {
velem = aelem = {canPlayType: noop};
}
var prefixes = ['moz', 'webkit', 'ms'];
function prefixed(property, context) {
if (!context) {
context = window;
}
try {
if (property in context) {
return context[property];
}
} catch(e) {
return false;
}
// Camel-case it.
property = property[0].toUpperCase() + property.substr(1);
for (var i = 0, e; e = prefixes[i++];) {
try {
if ((e + property) in context) {
return context[e + property];
}
} catch(err) {
return false;
}
}
}
var has_gum = prefixed('getUserMedia', navigator);
if (has_gum && navigator.mozGetUserMedia) {
// Gecko 18's gum is a noop.
try {
navigator.mozGetUserMedia(); // Should throw a TypeError.
has_gum = false;
} catch(e) {}
}
var audiocontext = window.webkitAudioContext || window.AudioContext;
var has_audiocontext = !!(audiocontext);
var capabilities = [
'mozApps' in navigator,
'mozApps' in navigator && navigator.mozApps.installPackage,
'mozPay' in navigator,
// FF 18 and earlier throw an exception on this key
(function() {try{return !!window.MozActivity;} catch(e) {return false;}})(),
'ondevicelight' in window,
'ArchiveReader' in window,
'battery' in navigator,
'mozBluetooth' in navigator,
'mozContacts' in navigator,
'getDeviceStorage' in navigator,
(function() { try{return window.mozIndexedDB || window.indexedDB;} catch(e) {return false;}})(),
'geolocation' in navigator && 'getCurrentPosition' in navigator.geolocation,
'addIdleObserver' in navigator && 'removeIdleObserver' in navigator,
'mozConnection' in navigator && (navigator.mozConnection.metered === true || navigator.mozConnection.metered === false),
'mozNetworkStats' in navigator,
'ondeviceproximity' in window,
'mozPush' in navigator || 'push' in navigator,
'ondeviceorientation' in window,
'mozTime' in navigator,
'vibrate' in navigator,
'mozFM' in navigator || 'mozFMRadio' in navigator,
'mozSms' in navigator,
!!(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch),
window.screen.width <= 540 && window.screen.height <= 960, // qHD support
!!aelem.canPlayType('audio/mpeg').replace(/^no$/, ''), // mp3 support
!!(window.Audio), // Audio Data API
has_audiocontext, // Web Audio API
!!velem.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,''), // H.264
!!velem.canPlayType('video/webm; codecs="vp8"').replace(/^no$/,''), // WebM
!!prefixed('cancelFullScreen', document), // Full Screen API
!!prefixed('getGamepads', navigator), // Gamepad API
!!(prefixed('persistentStorage') || window.StorageInfo), // Quota Management API
// WebRTC:
has_gum && !prefixed('cameras', navigator), // Can take photos
has_gum && has_audiocontext &&
!!((new audiocontext()).createMediaStreamSource), // Can record audio
has_gum && false, // XXX: Google WebRTC issue 2088
'MediaStream' in window,
'DataChannel' in window,
prefixed('RTCPeerConnection'),
prefixed('SpeechSynthesisEvent'), // WebSpeech Synthesis
prefixed('SpeechInputEvent'), // WebSpeech Input
prefixed('requestPointerLock', document.documentElement), // Pointer lock
prefixed('notification', navigator), // TODO: window.webkitNotifications?
prefixed('alarms', navigator), // Alarms
'mozSystem' in (new XMLHttpRequest()), // mozSystemXHR
prefixed('TCPSocket', navigator), // mozTCPSocket/mozTCPSocketServer
prefixed('mozInputMethod', navigator),
prefixed('mozMobileConnections', navigator)
];
var profile = parseInt(capabilities.map(function(x) {return !!x ? '1' : '0';}).join(''), 2).toString(16);
// Add a count.
profile += '.' + capabilities.length;
// Add a version number.
profile += '.4';
return {
capabilities: capabilities,
profile: profile
};
});

Просмотреть файл

@ -250,6 +250,11 @@ define('builder',
});
} else {
var done = function(data) {
if (signature.filters) {
signature.filters.forEach(function(filterName) {
data = env.filters[filterName](data);
});
}
document.getElementById(uid).innerHTML = data;
};
request.done(done).fail(function() {

Просмотреть файл

@ -1,8 +1,99 @@
define('cache', ['log', 'rewriters', 'storage'], function(log, rewriters, storage) {
define('cache',
['log', 'rewriters', 'settings', 'storage', 'user', 'utils', 'z'],
function(log, rewriters, settings, storage, user, utils, z) {
var console = log('cache');
var cache = {};
var cache_key = 'request_cache';
if (settings.offline_cache_enabled()) {
cache = JSON.parse(storage.getItem(cache_key) || '{}');
flush_expired();
}
// Persist the cache for whitelisted URLs.
window.addEventListener('beforeunload', save, false);
function get_ttl(url) {
// Returns TTL for an API URL in microseconds.
var path = utils.urlparse(url).pathname;
if (path in settings.offline_cache_whitelist) {
// Convert from seconds to microseconds.
return settings.offline_cache_whitelist[path] * 1000;
}
return null;
}
function save() {
if (!settings.offline_cache_enabled()) {
return;
}
var cache_to_save = {};
Object.keys(cache).forEach(function (url) {
// If there is a TTL assigned to this URL, then we can cache it.
if (get_ttl(url) !== null) {
cache_to_save[url] = cache[url];
}
});
// Trigger an event to save the cache. (We do size checks to see if
// the combined request+model cache is too large to persist.)
z.doc.trigger('save_cache', cache_key);
// Persist only if the data has changed.
var cache_to_save_str = JSON.stringify(cache_to_save);
if (storage.getItem(cache_key) !== cache_to_save_str) {
storage.setItem(cache_key, cache_to_save_str);
console.log('Persisting request cache');
}
}
function flush() {
cache = {};
}
function flush_signed() {
// This gets called when a user logs out, so we remove any requests
// with user data in them.
// First, we remove every signed URL from the request cache.
Object.keys(cache).forEach(function (url) {
if (url.indexOf('_user=' + user.get_token()) !== -1) {
console.log('Removing signed URL', url);
delete cache[url];
}
});
// Then, we persist the cache.
save();
}
function flush_expired() {
// This gets called once when the page loads to purge any expired
// persisted responses.
var now = +new Date();
var time;
var ttl = null;
Object.keys(cache).forEach(function (url) {
// Get the timestamp.
time = cache[url].__time;
// Get the TTL if this URL is allowed to be cached.
ttl = get_ttl(url);
// If the item is expired, remove it from the cache.
if (!time || time + ttl <= now) {
console.log('Removing expired URL', url);
return delete cache[url];
}
});
save();
}
function has(key) {
return key in cache;
@ -48,7 +139,7 @@ define('cache', ['log', 'rewriters', 'storage'], function(log, rewriters, storag
get: function(key) {
if (has(key)) {
return get(key);
} else if (key in storageKeys) {
} else {
var val = storage.getItem(persistentCachePrefix + key);
set(key, val);
return val;
@ -59,9 +150,7 @@ define('cache', ['log', 'rewriters', 'storage'], function(log, rewriters, storag
set(key, val);
},
bust: function(key) {
if (key in storageKeys) {
storage.removeItem(persistentCachePrefix + key);
}
storage.removeItem(persistentCachePrefix + key);
bust(key);
},
has: function(key) {
@ -100,15 +189,18 @@ define('cache', ['log', 'rewriters', 'storage'], function(log, rewriters, storag
}
return {
has: has,
get: get,
set: set,
bust: bust,
purge: purge,
attemptRewrite: rewrite,
bust: bust,
cache: cache,
flush: flush,
flush_expired: flush_expired,
flush_signed: flush_signed,
get: get,
get_ttl: get_ttl,
has: has,
persist: persistent,
purge: purge,
raw: cache,
persist: persistent
set: set
};
});

Просмотреть файл

@ -22,16 +22,15 @@ define('capabilities', [], function() {
'webactivities': !!(navigator.setMessageHandler || navigator.mozSetMessageHandler),
'firefoxOS': navigator.mozApps && navigator.mozApps.installPackage &&
navigator.userAgent.indexOf('Android') === -1 &&
navigator.userAgent.indexOf('Mobile') !== -1,
'persona': !!navigator.id,
(navigator.userAgent.indexOf('Mobile') !== -1 || navigator.userAgent.indexOf('Tablet') !== -1),
'phantom': navigator.userAgent.match(/Phantom/) // Don't use this if you can help it.
};
static_caps.persona = !!navigator.id && !static_caps.phantom;
static_caps.persona = function() { return (!!navigator.id || !!navigator.mozId) && !static_caps.phantom; };
// True if the login should inherit mobile behaviors such as allowUnverified.
// The _shimmed check is for B2G where identity is native (not shimmed).
static_caps.mobileLogin = static_caps.persona && (!navigator.id._shimmed || static_caps.firefoxAndroid);
static_caps.mobileLogin = function() { return static_caps.persona() && (!navigator.id._shimmed || static_caps.firefoxAndroid); };
static_caps.device_type = function() {
if (static_caps.firefoxOS) {

Просмотреть файл

@ -1,11 +1,14 @@
define('forms', ['jquery', 'z'], function($, z) {
define('forms', ['z'], function(z) {
function checkValid(form) {
if (form) {
$(form).filter(':not([novalidate])').find('button[type=submit]').attr('disabled', !form.checkValidity());
}
}
z.body.on('change keyup paste', 'input, select, textarea', function(e) {
// Note 'input' event is required for FF android (bug 977642)
z.body.on('change input', 'input, textarea', function(e) {
checkValid(e.target.form);
}).on('change', 'select', function(e) {
checkValid(e.target.form);
}).on('loaded decloak', function() {
$('form:not([novalidate])').each(function() {

Просмотреть файл

@ -117,6 +117,7 @@ define('helpers',
// Functions provided in the default context.
var helpers = {
api: require('urls').api.url,
apiHost: require('urls').api.host,
apiParams: require('urls').api.params,
anonApi: require('urls').api.unsigned.url,
anonApiParams: require('urls').api.unsigned.params,
@ -127,10 +128,11 @@ define('helpers',
_plural: make_safe(l10n.ngettext),
format: require('format').format,
settings: require('settings'),
capabilities: require('capabilities'),
user: userobj,
escape: utils.escape_,
len: function(x) {return x ? x.length || 0 : 0;},
len: function(x) {return x.length;},
max: Math.max,
min: Math.min,
range: _.range,

Просмотреть файл

@ -1,9 +1,11 @@
(function() {
// This is a little misleading. If you're using the Marketplace this is likely
// overridden below with body_langs. See bug 892741 for details.
var languages = [
'bg', 'ca', 'cs', 'de', 'el', 'en-US', 'es', 'eu', 'fr', 'ga-IE', 'hr',
'hu', 'it', 'ja', 'mk', 'nl', 'pl', 'pt-BR', 'ro', 'ru', 'sk', 'sr',
'sr-Latn', 'tr', 'zh-TW', 'dbg'
'bg', 'bn-BD', 'ca', 'cs', 'da', 'de', 'el', 'en-US', 'es', 'eu', 'fr',
'ga-IE', 'hr', 'hu', 'it', 'ja', 'ko', 'mk', 'nb-NO', 'nl', 'pl', 'pt-BR',
'ro', 'ru', 'sk', 'sq', 'sr', 'sr-Latn', 'tr', 'zh-CN', 'zh-TW', 'dbg'
];
var body_langs;
if (body_langs = document.body.getAttribute('data-languages')) {

Просмотреть файл

@ -182,6 +182,27 @@ exports.groupBy = function(obj, val) {
return result;
};
exports.toArray = function(obj) {
return Array.prototype.slice.call(obj);
};
exports.without = function(array) {
var result = [];
if (!array) {
return result;
}
var index = -1,
length = array.length,
contains = exports.toArray(arguments).slice(1);
while(++index < length) {
if(contains.indexOf(array[index]) === -1) {
result.push(array[index]);
}
}
return result;
};
exports.extend = function(obj, obj2) {
for(var k in obj2) {
obj[k] = obj2[k];
@ -233,6 +254,27 @@ exports.map = function(obj, func) {
return results;
};
exports.asyncParallel = function(funcs, done) {
var count = funcs.length,
result = new Array(count),
current = 0;
var makeNext = function(i) {
return function(res) {
result[i] = res;
current += 1;
if (current === count) {
done(result);
}
};
};
for (var i = 0; i < count; i++) {
funcs[i](makeNext(i));
}
};
exports.asyncIter = function(arr, iter, cb) {
var i = -1;
@ -270,6 +312,44 @@ exports.asyncFor = function(obj, iter, cb) {
next();
};
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function(array, searchElement /*, fromIndex */) {
if (array === null) {
throw new TypeError();
}
var t = Object(array);
var len = t.length >>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 2) {
n = Number(arguments[2]);
if (n != n) { // shortcut for verifying if it's NaN
n = 0;
} else if (n !== 0 && n !== Infinity && n !== -Infinity) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
return -1;
}
var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
for (; k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
};
}
if(!Array.prototype.map) {
Array.prototype.map = function() {
throw new Error("map is unimplemented for this js engine");
};
}
exports.keys = function(obj) {
if(Object.prototype.keys) {
return obj.keys();
@ -497,6 +577,17 @@ function memberLookup(obj, val) {
return obj[val];
}
function callWrap(obj, name, args) {
if(!obj) {
throw new Error('Unable to call `' + name + '`, which is undefined or falsey');
}
else if(typeof obj !== 'function') {
throw new Error('Unable to call `' + name + '`, which is not a function');
}
return obj.apply(this, args);
}
function contextOrFrameLookup(context, frame, name) {
var val = frame.lookup(name);
return (val !== undefined && val !== null) ?
@ -596,6 +687,7 @@ modules.runtime = {
suppressValue: suppressValue,
memberLookup: memberLookup,
contextOrFrameLookup: contextOrFrameLookup,
callWrap: callWrap,
handleError: handleError,
isArray: lib.isArray,
keys: lib.keys,
@ -1298,7 +1390,11 @@ nunjucks.configure = function(templatesPath, opts) {
opts = templatesPath;
templatesPath = null;
}
return e = new env.Environment(null, opts);
var noWatch = 'watch' in opts ? !opts.watch : false;
e = new env.Environment(null, opts);
return e;
};
nunjucks.render = function(name, ctx, cb) {

Просмотреть файл

@ -64,7 +64,7 @@ define('log', ['storage', 'utils'], function(storage, utils) {
// Have log('payments') but want log('payments', 'mock')?
// log('payments').tagged('mock') gives you the latter.
tagged: function(newTag) {
return logger(type, (tag ? tag + '][' : '') + newTag, onlog);
return logger(type, tag + '][' + newTag, onlog);
}
};
};

Просмотреть файл

@ -1,6 +1,6 @@
define('login',
['capabilities', 'defer', 'jquery', 'log', 'notification', 'settings', 'underscore', 'urls', 'user', 'requests', 'z'],
function(capabilities, defer, $, log, notification, settings, _, urls, user, requests, z) {
['cache', 'capabilities', 'defer', 'jquery', 'log', 'notification', 'settings', 'underscore', 'urls', 'user', 'utils', 'requests', 'z'],
function(cache, capabilities, defer, $, log, notification, settings, _, urls, user, utils, requests, z) {
var console = log('login');
@ -25,18 +25,19 @@ define('login',
e.preventDefault();
requests.del(urls.api.url('logout')).done(function() {
// Moved here from the onlogout callback for now until
// https://github.com/mozilla/browserid/issues/3229
// gets fixed.
cache.flush_signed();
user.clear_token();
z.body.removeClass('logged-in');
z.page.trigger('reload_chrome').trigger('before_logout');
if (capabilities.persona) {
if (capabilities.persona()) {
console.log('Triggering Persona logout');
navigator.id.logout();
}
// Moved here from the onlogout callback for now until
// https://github.com/mozilla/browserid/issues/3229
// gets fixed.
if (!z.context.dont_reload_on_login) {
require('views').reload().done(function(){
z.page.trigger('logged_out');
@ -72,7 +73,7 @@ define('login',
// See bug 910938.
opt.experimental_forceIssuer = settings.persona_unverified_issuer;
}
if (capabilities.mobileLogin) {
if (capabilities.mobileLogin()) {
// On mobile we don't require new accounts to verify their email.
// On desktop, we do.
opt.experimental_allowUnverified = true;
@ -81,10 +82,13 @@ define('login',
console.log('Not allowing unverified emails');
}
if (capabilities.persona) {
console.log('Requesting login from Persona');
navigator.id.request(opt);
}
persona_loaded.done(function() {
if (capabilities.persona()) {
console.log('Requesting login from Persona');
navigator.id.request(opt);
}
});
return def.promise();
}
@ -94,7 +98,7 @@ define('login',
var data = {
assertion: assertion,
audience: window.location.protocol + '//' + window.location.host,
is_mobile: capabilities.mobileLogin
is_mobile: capabilities.mobileLogin()
};
z.page.trigger('before_login');
@ -146,24 +150,66 @@ define('login',
});
}
var email = user.get_setting('email') || '';
if (email) {
console.log('Detected user', email);
} else {
console.log('No previous user detected');
var persona_def = defer.Deferred();
var persona_loaded = persona_def.promise();
var persona_loading_start = +(new Date());
var persona_loading_time = 0;
var persona_step = 25; // 25 milliseconds
var GET = utils.getVars();
var persona_shim_included = $('script[src="' + settings.persona_shim_url + '"]').length;
// If for some reason Zamboni got `?nativepersona=true` but we actually
// don't have native Persona, then let's inject a script to load the shim.
if (!persona_shim_included && !capabilities.persona()) {
var s = document.createElement('script');
s.async = true;
s.src = settings.persona_shim_url;
document.body.appendChild(s);
}
if (capabilities.persona) {
console.log('Calling navigator.id.watch');
navigator.id.watch({
loggedInUser: email,
onlogin: gotVerifiedEmail,
onlogout: function() {
z.body.removeClass('logged-in');
z.page.trigger('reload_chrome').trigger('logout');
}
var persona_interval = setInterval(function() {
persona_loading_time = +(new Date()) - persona_loading_start;
if (capabilities.persona()) {
console.log('Persona loaded (' + persona_loading_time / 1000 + 's)');
persona_def.resolve();
clearInterval(persona_interval);
} else if (persona_loading_time >= settings.persona_timeout) {
console.error('Persona timeout (' + persona_loading_time / 1000 + 's)');
persona_def.reject();
clearInterval(persona_interval);
}
}, persona_step);
persona_loaded.done(function() {
// This lets us change the cursor for the "Sign in" link.
z.body.addClass('persona-loaded');
var email = user.get_setting('email') || '';
if (email) {
console.log('Detected user', email);
} else {
console.log('No previous user detected');
}
if (capabilities.persona()) {
console.log('Calling navigator.id.watch');
navigator.id.watch({
loggedInUser: email,
onlogin: gotVerifiedEmail,
onlogout: function() {
z.body.removeClass('logged-in');
z.page.trigger('reload_chrome').trigger('logout');
}
});
}
}).fail(function() {
notification.notification({
message: gettext('Persona cannot be reached. Try again later.')
});
}
});
return {login: startLogin};
});

Просмотреть файл

@ -1,10 +1,68 @@
define('models', ['defer', 'log', 'requests', 'settings', 'underscore'], function(defer, log, requests, settings, _) {
define('models',
['cache', 'defer', 'log', 'requests', 'settings', 'storage', 'underscore', 'z'],
function(cache, defer, log, requests, settings, storage, _, z) {
var console = log('model');
// {'type': {'<id>': object}}
var cache_key = 'model_cache';
var data_store = {};
if (settings.offline_cache_enabled()) {
data_store = JSON.parse(storage.getItem(cache_key) || '{}');
}
// Persist the model cache.
window.addEventListener('beforeunload', save, false);
z.doc.on('saving_offline_cache', function (e, cache_key) {
// Really, this should be an LRU cache but the builder has an
// expectation that a hit to the request cache means that the models
// have been casted and exist already in the model cache too.
//
// It gets too complicated having one LRU cache for the request cache
// and then independent LRU caches for app, category, and collection
// model caches. It's fine. It's fine.
var data = {
'request_cache': JSON.stringify(cache.cache),
'model_cache': JSON.stringify(data_store)
};
var size = (JSON.stringify(data.request_cache).length +
JSON.stringify(data.model_cache).length);
if (size >= settings.offline_cache_limit) {
console.warn('Quota exceeded for request/model offline cache; ' +
'flushing cache');
cache.flush();
flush();
storage.setItem(cache_key, data_store_str);
} else {
// Persist only if the data has changed.
var data_store_str = data[cache_key];
if (storage.getItem(cache_key) !== data_store_str) {
storage.setItem(cache_key, data_store_str);
console.log('Persisting model cache');
}
}
});
function flush() {
// Purge cache for every type of model.
data_store = {};
}
function save() {
z.doc.trigger('save_cache', cache_key);
// Persist only if the data has changed.
var data_store_str = JSON.stringify(data_store);
if (storage.getItem(cache_key) !== data_store_str) {
storage.setItem(cache_key, data_store_str);
console.log('Persisting model cache');
}
}
var prototypes = settings.model_prototypes;
return function(type) {
@ -96,11 +154,13 @@ define('models', ['defer', 'log', 'requests', 'settings', 'underscore'], functio
return {
cast: cast,
uncast: uncast,
data_store: data_store,
del: del,
flush: flush,
get: get,
lookup: lookup,
purge: purge,
del: del
uncast: uncast
};
};

Просмотреть файл

@ -10,6 +10,7 @@ define('navigation',
{path: '/', type: 'root'}
];
var initialized = false;
var scrollTimer;
function extract_nav_url(url) {
// This function returns the URL that we should use for navigation.
@ -69,8 +70,16 @@ define('navigation',
}
top = state.scrollTop;
}
console.log('Setting scroll to', top);
window.scrollTo(0, top);
// Introduce small delay to ensure content
// is ready to scroll. (Bug 976466)
if (scrollTimer) {
window.clearTimeout(scrollTimer);
}
scrollTimer = window.setTimeout(function() {
console.log('Setting scroll to', top);
window.scrollTo(0, top);
}, 250);
// Clean the path's parameters.
// /foo/bar?foo=bar&q=blah -> /foo/bar?q=blah
@ -190,7 +199,7 @@ define('navigation',
el.getAttribute('rel') === 'external';
}
z.doc.on('click', 'a', function(e) {
z.body.on('click', 'a', function(e) {
var href = this.getAttribute('href');
var $elm = $(this);
var preserveScrollData = $elm.data('preserveScroll');
@ -214,16 +223,47 @@ define('navigation',
}
var state = e.originalEvent.state;
if (state) {
console.log('popstate navigate');
navigate(state.path, true, state);
if (state.closeModalName) {
console.log('popstate closing modal');
cleanupModal(state.closeModalName);
} else {
console.log('popstate navigate');
navigate(state.path, true, state);
}
}
}).on('submit', 'form', function() {
console.error("Form submissions are not allowed.");
return false;
});
function modal(name) {
console.log('Opening modal', name);
stack[0].closeModalName = name;
history.replaceState(stack[0], false, stack[0].path);
history.pushState(null, name, '#' + name);
var path = window.location.href + '#' + name;
stack.unshift({path: path, type: 'modal', name: name});
}
function cleanupModal(name) {
stack.shift();
delete stack[0].closeModalName;
z.win.trigger('closeModal', name);
}
function closeModal(name) {
if (stack[0].type === 'modal' && stack[0].name === name) {
console.log('Closing modal', name);
history.back();
} else {
console.log('Attempted to close modal', name, 'that was not open');
}
}
return {
'back': back,
'modal': modal,
'closeModal': closeModal,
'stack': function() {return stack;},
'navigationFilter': navigationFilter,
'extract_nav_url': extract_nav_url

Просмотреть файл

@ -1,6 +1,6 @@
define('requests',
['cache', 'defer', 'log', 'utils'],
function(cache, defer, log, utils) {
['cache', 'defer', 'log', 'settings', 'utils'],
function(cache, defer, log, settings, utils) {
var console = log('req');
@ -96,32 +96,58 @@ define('requests',
return def;
}
function get(url, nocache, persistent) {
// During a single session, we never want to fetch the same URL more than
// once. Because our persistent offline cache does XHRs in the background
// to keep the cache fresh, we want to do that only once per session. In
// order to do all this magic, we have to keep an array of all of the URLs
// we hit per session.
var urls_fetched = {};
function get(url, nocache) {
var cache_offline = settings.offline_cache_enabled();
var cached;
if (cache.has(url) && !nocache) {
console.log('GETing from cache', url);
cached = cache.get(url);
} else if (cache.persist.has(url) && persistent && !nocache) {
console.log('GETing from persistent cache', url);
cached = cache.persist.get(url);
}
if (cached) {
return defer.Deferred()
.resolve(cached)
.promise({__cached: true});
}
return _get.apply(this, arguments, persistent);
}
function _get(url, nocache, persistent) {
var def_cached;
if (cached) {
def_cached = defer.Deferred()
.resolve(cached)
.promise({__cached: true});
if (!cache_offline || url in urls_fetched) {
// If we don't need to make an XHR in the background to update
// the cache, then let's bail now.
return def_cached;
}
}
console.log('GETing', url);
return ajax('GET', url).done(function(data, xhr) {
urls_fetched[url] = null;
var def_ajax = ajax('GET', url).done(function(data) {
console.log('GOT', url);
if (!nocache) {
data.__time = +(new Date());
cache.set(url, data);
if (persistent) cache.persist.set(url, data);
}
if (cached && cache_offline) {
console.log('Updating request cache', url);
}
});
if (cached && cache_offline) {
// If the response was cached, we still want to fire off the
// AJAX request so the cache can get updated in the background,
// but let's resolve this deferred with the cached response
// so the request pool can get closed and the builder can render
// the template for the `defer` block.
return def_cached;
}
return def_ajax;
}
function handle_errors(xhr, type, status) {
@ -175,6 +201,7 @@ define('requests',
var initiated = 0;
var marked_to_finish = false;
var closed = false;
var failed = false;
function finish() {
if (closed) {
@ -185,11 +212,7 @@ define('requests',
closed = true;
// Resolve the deferred whenevs.
if (window.setImmediate) {
setImmediate(def.resolve);
} else {
setTimeout(def.resolve, 0);
}
setTimeout(failed ? def.reject : def.resolve, 0);
}
}
@ -202,6 +225,9 @@ define('requests',
var req = func.apply(this, args);
initiated++;
requests.push(req);
req.fail(function() {
failed = true;
});
req.always(function() {
initiated--;
finish();

Просмотреть файл

@ -1,6 +1,14 @@
define('settings', ['l10n', 'settings_local', 'underscore'], function(l10n, settings_local, _) {
var gettext = l10n.gettext;
function offline_cache_enabled() {
var storage = require('storage');
if (storage.getItem('offline_cache_disabled') || require('capabilities').phantom) {
return false;
}
return window.location.search.indexOf('cache=false') === -1;
}
return _.defaults(settings_local, {
app_name: 'commonplace app',
init_module: 'main',
@ -11,6 +19,15 @@ define('settings', ['l10n', 'settings_local', 'underscore'], function(l10n, sett
param_whitelist: ['q', 'sort'],
api_param_blacklist: null,
api_cdn_whitelist: {},
// These are the only URLs that should be cached
// (key: URL; value: TTL [time to live] in seconds).
// Keep in mind that the cache is always refreshed asynchronously;
// these TTLs apply to only when the app is first launched.
offline_cache_whitelist: {},
offline_cache_enabled: offline_cache_enabled,
offline_cache_limit: 1024 * 1024 * 4, // 4 MB
model_prototypes: {},

Просмотреть файл

@ -1,6 +1,20 @@
define('urls',
['format', 'routes_api', 'routes_api_args', 'settings', 'user', 'utils'],
function(format, api_endpoints, api_args, settings, user) {
['format', 'log', 'routes_api', 'routes_api_args', 'settings', 'user', 'utils'],
function(format, log, api_endpoints, api_args, settings, user, utils) {
var console = log('urls');
// The CDN URL is the same as the media URL but without the `/media/` path.
if ('media_url' in settings) {
var a = document.createElement('a');
a.href = settings.media_url;
settings.cdn_url = a.protocol + '//' + a.host;
console.log('Using settings.media_url: ' + settings.media_url);
console.log('Changed settings.cdn_url: ' + settings.cdn_url);
} else {
settings.cdn_url = settings.api_url;
console.log('Changed settings.cdn_url to settings.api_url: ' + settings.api_url);
}
var group_pattern = /\([^\)]+\)/;
var optional_pattern = /(\(.*\)|\[.*\]|.)\?/g;
@ -44,7 +58,7 @@ define('urls',
args._user = user.get_token();
}
_removeBlacklistedParams(args);
return require('utils').urlparams(out, args);
return utils.urlparams(out, args);
};
}
@ -53,7 +67,7 @@ define('urls',
var out = func.apply(this, arguments);
var args = api_args();
_removeBlacklistedParams(args);
return require('utils').urlparams(out, args);
return utils.urlparams(out, args);
};
}
@ -71,9 +85,12 @@ define('urls',
console.error('Invalid API endpoint: ' + endpoint);
return '';
}
var url = settings.api_url + format.format(api_endpoints[endpoint], args || []);
var path = format.format(api_endpoints[endpoint], args || []);
var url = apiHost(path) + path;
if (params) {
return require('utils').urlparams(url, params);
return utils.urlparams(url, params);
}
return url;
}
@ -82,6 +99,16 @@ define('urls',
return api(endpoint, [], params);
}
function apiHost(path) {
// If the API URL is already reversed, then here's where we determine
// whether that URL gets served from the API or CDN.
var host = settings.api_url;
if (utils.baseurl(path) in settings.api_cdn_whitelist) {
host = settings.cdn_url;
}
return host;
}
function media(path) {
var media_url = settings.media_url;
if (media_url[media_url.length - 1] !== '/') {
@ -97,6 +124,7 @@ define('urls',
reverse: reverse,
api: {
url: _userArgs(api),
host: apiHost,
params: _userArgs(apiParams),
sign: _userArgs(function(url) {return url;}),
unsign: _anonymousArgs(function(url) {return url;}),
@ -106,6 +134,7 @@ define('urls',
},
base: {
url: api,
host: apiHost,
params: apiParams
}
},

Просмотреть файл

@ -33,7 +33,8 @@ define('utils', ['jquery', 'l10n', 'underscore'], function($, l10n, _) {
var $cc = $(this);
$cc.closest('form')
.find('#' + $cc.data('for'))
.on('keyup blur', _.throttle(function() {countChars(this, $cc);}, 250))
// Note 'input' event is need for FF android see (bug 976262)
.on('input blur', _.throttle(function() {countChars(this, $cc);}, 250))
.trigger('blur');
});
}
@ -179,6 +180,13 @@ define('utils', ['jquery', 'l10n', 'underscore'], function($, l10n, _) {
return 'other';
}
var a = document.createElement('a');
function urlparse(url) {
a.href = url;
return a;
}
return {
'_pd': _pd,
'baseurl': baseurl,
@ -193,6 +201,7 @@ define('utils', ['jquery', 'l10n', 'underscore'], function($, l10n, _) {
'slugify': slugify,
'urlencode': urlencode,
'urlparams': urlparams,
'urlparse': urlparse,
'urlunparam': urlunparam,
'translate': translate
};

Просмотреть файл

@ -1,13 +1,36 @@
define('views/debug',
['cache', 'capabilities', 'log', 'notification', 'requests', 'settings', 'storage', 'utils', 'z'],
function(cache, capabilities, log, notification, requests, settings, storage, utils, z) {
['buckets', 'cache', 'capabilities', 'log', 'models', 'notification', 'requests', 'settings', 'storage', 'user', 'utils', 'z'],
function(buckets, cache, capabilities, log, models, notification, requests, settings, storage, user, utils, z) {
'use strict';
var persistent_console_debug = log.persistent('debug', 'change');
var persistent_console_network = log.persistent('mobilenetwork', 'change');
var label = $(document.getElementById('debug-status'));
z.doc.on('click', '#clear-localstorage', function(e) {
storage.clear();
notification.notification({message: 'localStorage cleared', timeout: 1000});
}).on('click', '#enable-offline-cache', function() {
storage.removeItem('offline_cache_disabled');
persistent_console_debug.log('Offline cache enabled:', new Date());
require('views').reload();
notification.notification({message: 'Offline cache enabled', timeout: 1000});
}).on('click', '#disable-offline-cache', function() {
storage.setItem('offline_cache_disabled', '1');
persistent_console_debug.log('Offline cache disabled:', new Date());
require('views').reload();
notification.notification({message: 'Offline cache disabled', timeout: 1000});
}).on('click', '#clear-offline-cache', function() {
cache.flush();
// This actually flushes all model caches.
models('app').flush();
persistent_console_debug.log('Offline cache cleared:', new Date());
notification.notification({message: 'Offline cache cleared', timeout: 1000});
window.location.reload();
}).on('click', '#clear-cookies', function() {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
@ -17,6 +40,10 @@ define('views/debug',
}
notification.notification({message: 'cookies cleared', timeout: 1000});
}).on('click', '#nukecounter', function(e) {
storage.removeItem('newscounter');
notification.notification({message: 'newscounter reset', timeout: 1000});
}).on('click', '.cache-menu a', function(e) {
e.preventDefault();
var data = cache.get($(this).data('url'));
@ -33,7 +60,8 @@ define('views/debug',
persistent_logs: log.persistent.all,
capabilities: capabilities,
settings: settings,
report_version: 1.0
report_version: 1.0,
profile: buckets.profile
})};
requests.post('https://ashes.paas.allizom.org/post_report', data).done(function(data) {
notification.notification({
@ -41,17 +69,40 @@ define('views/debug',
timeout: 30000
});
});
}).on('change', '#debug-page select[name=region]', function(e) {
var val = $(this).val();
var current_region = user.get_setting('region_override');
if (current_region !== val) {
persistent_console_network.log('Manual region override change:', current_region, '→', val);
}
user.update_settings({region_override: val});
z.page.trigger('reload_chrome');
notification.notification({message: 'Region updated to ' + (settings.REGION_CHOICES_SLUG[val] || '---')});
}).on('change', '#debug-page select[name=carrier]', function(e) {
var val = $(this).val();
var current_carrier = user.get_setting('carrier_override');
if (current_carrier !== val) {
persistent_console_network.log('Manual carrier override change:', current_carrier, '→', val);
}
user.update_settings({carrier_override: val});
z.page.trigger('reload_chrome');
notification.notification({message: 'Carrier updated to ' + val});
});
return function(builder, args) {
var recent_logs = log.get_recent(100);
builder.start('debug.html', {
carriers: require('mobilenetwork').carriers,
cache: cache.raw,
capabilities: capabilities,
profile: buckets.profile,
recent_logs: recent_logs,
persistent_logs: log.persistent.all,
filter: log.filter
filter: log.filter,
request_cache: JSON.parse(storage.getItem('request_cache') || '{}')
});
builder.z('type', 'leaf debug');

Просмотреть файл

@ -1,4 +1,4 @@
<section class="main infobox prose">
<section class="main infobox prose" id="debug-page">
<div>
<style>
dt {
@ -8,6 +8,12 @@
dd {
float: left;
}
#debug-page label {
font-size: 18px;
}
#debug-page select {
margin-left: 10px;
}
</style>
<h2>Debug</h2>
<p>
@ -18,16 +24,61 @@
<button class="button" id="clear-localstorage">Clear <code>localStorage</code></button>
</p>
{% if settings.offline_cache_enabled() %}
<p>
<button class="button" id="disable-offline-cache">Disable offline cache</button>
</p>
{% else %}
<p>
<button class="button" id="enable-offline-cache">Enable offline cache</button>
</p>
{% endif %}
<p>
<button class="button" id="clear-offline-cache">Clear offline cache</button>
</p>
<p>
<button class="button" id="clear-cookies">Clear cookies</button>
</p>
<p>
<button class="button" id="nukecounter">Clear <code>newscounter</code></button>
</p>
<p>
<label>
Region Override
<select name="region" id="region">
{% set user_region = user.get_setting('region_override') %}
<option value=""{{ ' selected' if not user_region }}>---</option>
{% for code, region in REGIONS.items() %}
<option value="{{ code }}"{{ ' selected' if code == user_region }}>
{{ region }}</option>
{% endfor %}
</select>
</label>
</p>
<p>
<label>
Carrier Override
<select name="carrier" id="carrier">
{% set user_carrier = user.get_setting('carrier_override') %}
<option value=""{{ ' selected' if not user_carrier }}>---</option>
{% for carrier in carriers %}
<option{{ ' selected' if carrier == user_carrier }}>{{ carrier }}</option>
{% endfor %}
</select>
</label>
</p>
<h3>Site Settings</h3>
<dl class="site-settings c">
{% for setting in settings.items() %}
<dt>{{ setting[0] }}</dt>
<dd>{{ setting[1] or '––' }}</dd>
<dd>{{ '—— truncated ——' if setting[0] == 'persona_site_logo' else setting[1] or '——' }}</dd>
{% endfor %}
</dl>
@ -47,6 +98,12 @@
<dt>{{ cap[0] }}</dt>
<dd>{{ cap[1] }}</dd>
{% endfor %}
{% for k, v in screen.items() %}
<dt>window.screen.{{ k }}</dt>
<dd>{{ v }}</dd>
{% endfor %}
<dt>Feature Profile</dt>
<dd>{{ profile }} (<a href="/debug/features">view feature profile information</a>)</dd>
</dl>
<h3>Cache</h3>
@ -75,5 +132,15 @@
{% endfor %}
</ol>
{% endfor %}
<h3>Offline Cache</h3>
<ol>
{% for url, response in request_cache.items() %}
<li>{{ url }}</li>
{% endfor %}
</ol>
<pre>
{{ request_cache|stringify(null, 2) }}
</pre>
</div>
</section>

Просмотреть файл

@ -4,6 +4,7 @@ var _ = require('underscore');
var assert = a.assert;
var eq_ = a.eq_;
var eeq_ = a.eeq_;
var feq_ = a.feq_;
var mock = a.mock;
var cache = require('cache');
@ -67,7 +68,11 @@ test('cache purge', function(done) {
test('cache purge filter', function(done) {
mock(
'cache',
{},
{
settings: {
offline_cache_enabled: function () { return false; }
}
},
function(cache) {
var key = 'test2:';
var str = 'poop';
@ -243,4 +248,108 @@ test('cache deep rewrite on set', function(done) {
);
});
test('cache get_ttl', function(done) {
mock(
'cache',
{
settings: {
offline_cache_enabled: function () { return true; },
offline_cache_whitelist: {
'/api/v1/fireplace/consumer-info/': 60 * 60, // 1 hour in seconds
'/api/v1/fireplace/search/featured/': 60 * 60 * 6, // 6 hours
'/api/v1/apps/category/': 60 * 60 * 24 // 1 day
}
}
},
function (cache) {
eq_(cache.get_ttl('https://omg.org/api/v1/fireplace/consumer-info/'),
60 * 60 * 1000); // 1 hour in microseconds
eq_(cache.get_ttl('https://omg.org/api/v1/apps/category/'),
60 * 60 * 24 * 1000); // 1 hour in microseconds
eq_(cache.get_ttl('https://omg.org/api/v1/swag/yolo/foreva/'), null);
done();
}
);
});
test('cache flush_signed', function(done) {
mock(
'cache',
{
user: {
logged_in: function() { return true; },
get_setting: function(x) {},
get_token: function() { return 'SwaggasaurusRex';}
}
},
function (cache) {
var data = 'ratchet data';
var signed_url = 'https://omg.org/api/v1/app/yolo/?_user=SwaggasaurusRex';
cache.set(signed_url, data);
eq_(cache.get(signed_url), data);
var unsigned_url = 'https://omg.org/api/v1/app/swag/';
cache.set(unsigned_url, data);
eq_(cache.get(unsigned_url), data);
feq_(Object.keys(cache.cache).sort(), [unsigned_url, signed_url]);
// Calling this should clear all cache keys whose URLs contain
// `_user=<token>`.
cache.flush_signed();
feq_(Object.keys(cache.cache), [unsigned_url]);
done();
}
);
});
test('cache flush_expired', function(done) {
mock(
'cache',
{
settings: {
offline_cache_enabled: function () { return true; },
offline_cache_whitelist: {
'/api/v1/fireplace/consumer-info/': 60 * 60, // 1 hour in seconds
'/api/v1/fireplace/search/featured/': 60 * 60 * 6, // 6 hours
'/api/v1/apps/category/': 60 * 60 * 24 // 1 day
}
}
},
function (cache) {
// Both were just added and unexpired ...
cache.set('https://omg.org/api/v1/fireplace/consumer-info/', {
'__time': +new Date()
});
cache.set('https://omg.org/api/v1/fireplace/search/featured/', {
'__time': +new Date()
});
cache.flush_expired();
assert(cache.has('https://omg.org/api/v1/fireplace/consumer-info/'));
assert(cache.has('https://omg.org/api/v1/fireplace/search/featured/'));
// Neither has expired ...
cache.set('https://omg.org/api/v1/fireplace/consumer-info/', {
'__time': +new Date() - (60 * 59 * 1000) // 59 min ago in microseconds
});
cache.flush_expired();
assert(cache.has('https://omg.org/api/v1/fireplace/consumer-info/'));
assert(cache.has('https://omg.org/api/v1/fireplace/search/featured/'));
// One has expired!
cache.set('https://omg.org/api/v1/fireplace/consumer-info/', {
'__time': +new Date() - (60 * 65 * 1000) // 1 hr 5 min ago in microseconds
});
cache.flush_expired();
assert(!cache.has('https://omg.org/api/v1/fireplace/consumer-info/'));
assert(cache.has('https://omg.org/api/v1/fireplace/search/featured/'));
done();
}
);
});
})();

Просмотреть файл

@ -20,7 +20,12 @@ test('model invalid type', function(done, fail) {
test('model cast/lookup/purge', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id', 'dummy2': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id', 'dummy2': 'id'}
}
},
function(models) {
var d1 = models('dummy');
var d2 = models('dummy2');
@ -57,7 +62,12 @@ test('model cast/lookup/purge', function(done, fail) {
test('model cast/lookup/delete', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
d1.cast({
@ -79,7 +89,12 @@ test('model cast/lookup/delete', function(done, fail) {
test('model cast/lookup/delete val', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
d1.cast({
@ -101,7 +116,12 @@ test('model cast/lookup/delete val', function(done, fail) {
test('model cast/uncast', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
@ -125,7 +145,12 @@ test('model cast/uncast', function(done, fail) {
test('model cast/uncast lists', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
@ -165,7 +190,10 @@ test('model get hit', function(done, fail) {
'models',
{
requests: {get: function(x) {return 'surprise! ' + x;}},
settings: {model_prototypes: {'dummy': 'id'}}
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
@ -190,7 +218,10 @@ test('model get miss', function(done, fail) {
'models',
{
requests: {get: function(x) {return 'surprise! ' + x;}},
settings: {model_prototypes: {'dummy': 'id'}}
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
@ -207,7 +238,10 @@ test('model get getter', function(done, fail) {
'models',
{
requests: {get: function(x) {return "not the droids you're looking for";}},
settings: {model_prototypes: {'dummy': 'id'}}
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
@ -224,7 +258,12 @@ test('model get getter', function(done, fail) {
test('model lookup by', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
@ -250,7 +289,12 @@ test('model lookup by', function(done, fail) {
test('model lookup miss', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');
@ -270,7 +314,12 @@ test('model lookup miss', function(done, fail) {
test('model cast list', function(done, fail) {
mock(
'models',
{settings: {model_prototypes: {'dummy': 'id'}}},
{
settings: {
offline_cache_enabled: function () { return false; },
model_prototypes: {'dummy': 'id'}
}
},
function(models) {
var d1 = models('dummy');

Просмотреть файл

@ -77,12 +77,13 @@ test('api url', function(done, fail) {
{
capabilities: {firefoxOS: true, widescreen: function() { return false; }, touch: 'foo'},
routes_api: {'homepage': '/foo/homepage'},
routes_api_args: function() {return function() {return function() {return {foo: 'bar'};};};}, // Functions get pre-evaluated.
settings: {api_url: 'api:'}
settings: {
api_url: 'api:',
api_cdn_whitelist: {},
}
}, function(urls) {
var homepage_url = urls.api.url('homepage');
eq_(homepage_url.substr(0, 17), 'api:/foo/homepage');
contains(homepage_url, 'foo=bar');
done();
},
fail
@ -95,8 +96,10 @@ test('api url signage', function(done, fail) {
{
capabilities: {firefoxOS: true, widescreen: function() { return false; }, touch: 'foo'},
routes_api: {'homepage': '/foo/homepage'},
routes_api_args: function() {return function() {return function() {return {foo: 'bar'};};};}, // Functions get pre-evaluated.
settings: {api_url: 'api:'},
settings: {
api_url: 'api:',
api_cdn_whitelist: {}
},
user: {
logged_in: function() { return true; },
get_setting: function(x) {},
@ -127,7 +130,11 @@ test('api url blacklist', function(done, fail) {
{
capabilities: {firefoxOS: true, widescreen: function() { return false; }, touch: 'foo'},
routes_api: {'homepage': '/foo/homepage'},
settings: {api_url: 'api:', api_param_blacklist: ['region']}
settings: {
api_cdn_whitelist: {},
api_url: 'api:',
api_param_blacklist: ['region']
}
}, function(urls) {
var homepage_url = urls.api.url('homepage');
eq_(homepage_url.substr(0, 17), 'api:/foo/homepage');
@ -138,12 +145,45 @@ test('api url blacklist', function(done, fail) {
);
});
test('api url CDN whitelist', function(done, fail) {
mock(
'urls',
{
routes_api: {
'homepage': '/api/v1/homepage/',
'search': '/api/v1/fireplace/search/?swag=yolo'
},
settings: {
api_url: 'api:',
api_cdn_whitelist: {
'/api/v1/fireplace/search/': 60, // 1 minute
'/api/v1/fireplace/search/featured/': 60 * 2, // 2 minutes
},
media_url: 'http://cdn.so.fast.omg.org'
}
}, function(urls) {
var homepage_url = urls.api.url('homepage');
eq_(homepage_url.substr(0, 21), 'api:/api/v1/homepage/');
var search_url = urls.api.url('search');
eq_(search_url.substr(0, 51),
'http://cdn.so.fast.omg.org/api/v1/fireplace/search/');
done();
},
fail
);
});
test('api url params', function(done, fail) {
mock(
'urls',
{
routes_api: {'homepage': '/foo/asdf'},
settings: {api_url: 'api:'}
settings: {
api_url: 'api:',
api_cdn_whitelist: {}
}
}, function(urls) {
var homepage_url = urls.api.params('homepage', {q: 'poop'});
eq_(homepage_url.substr(0, 13), 'api:/foo/asdf');

Просмотреть файл

@ -135,9 +135,6 @@ test('translate', function(done) {
eq_(filters.translate({'blah': '3'}, dlobj, 'es-PD'), '3');
eq_(filters.translate({'foo': 'bar', 'en-US': '3'}, null, 'es-PD'), '3');
eq_(filters.translate({}, dlobj, 'es-PD'), '');
eq_(filters.translate('', dlobj, 'es-PD'), '');
eq_(filters.translate(null, dlobj, 'es-PD'), '');
eq_(filters.translate(undefined, dlobj, 'es-PD'), '');
done();
});