From 3a3542dcfc37934eda6c11f034bf0918e9261c2d Mon Sep 17 00:00:00 2001 From: Evelyn Hung Date: Mon, 21 Sep 2015 20:06:07 +0800 Subject: [PATCH] Bug 1206641 - backport BaseModule to TV system app --- tv_apps/smart-system/index.html | 3 + tv_apps/smart-system/js/base_module.js | 787 ++++++++++++++++++ tv_apps/smart-system/js/bootstrap.js | 7 +- tv_apps/smart-system/js/core.js | 70 ++ tv_apps/smart-system/js/service.js | 221 +++++ tv_apps/smart-system/js/settings_core.js | 157 ++++ .../test/unit/base_module_test.js | 747 +++++++++++++++++ tv_apps/smart-system/test/unit/deferred.js | 12 + 8 files changed, 2003 insertions(+), 1 deletion(-) create mode 100644 tv_apps/smart-system/js/base_module.js create mode 100644 tv_apps/smart-system/js/core.js create mode 100644 tv_apps/smart-system/js/settings_core.js create mode 100644 tv_apps/smart-system/test/unit/base_module_test.js create mode 100644 tv_apps/smart-system/test/unit/deferred.js diff --git a/tv_apps/smart-system/index.html b/tv_apps/smart-system/index.html index b9aa75bd1d00..cd33bdf0dabd 100644 --- a/tv_apps/smart-system/index.html +++ b/tv_apps/smart-system/index.html @@ -129,6 +129,9 @@ + + + diff --git a/tv_apps/smart-system/js/base_module.js b/tv_apps/smart-system/js/base_module.js new file mode 100644 index 000000000000..59db44bf8964 --- /dev/null +++ b/tv_apps/smart-system/js/base_module.js @@ -0,0 +1,787 @@ +/* global LazyLoader, DUMP */ +'use strict'; + +(function(exports) { + /** + * Turn of this flag to debug all BaseModule based modules. + * @type {Boolean} + */ + var GLOBAL_DEBUG = false; + + /** + * `_GAIA_DEVICE_TYPE_` is a placeholder and will be replaced by real + * device type in build time, `system/build/build.js` does the trick. + */ + var DEVICE_TYPE = '_GAIA_DEVICE_TYPE_'; + /** + * This is used to store the constructors which are created + * via BaseModule.create(). + * constructor.name => constructor + * @type {Object} + */ + var AVAILABLE_MODULES = {}; + + /** + * BaseModule is a class skeleton which helps you to build a module with + * * Centralized event handler + * * Centralized settings observation + * * Sub modules management including loading and starting + * * Import preload files + * * DOM rendering + * * Consistent logging function with System boot time and module name + * * Common publishing interface + * @class BaseModule + */ + var BaseModule = function() {}; + + /** + * The sub modules belong to this module. + * BaseModule will load and then start these sub modules automatically. + * The expressions can include path (e.g. 'path/to/ModuleName'). BaseModule + * will load them from specified subdirectory. However, the module names + * (without path) should be unique even they're under different folders. + * + * You can provide an object: + * Define a list of default type modules are able to be loaded in anytime. + * Define a list of device type modules can be loaded when running on a given + * device-type. + * { + * default: ['base_module1', 'base_module2', ...], + * device-type: ['module1', 'module2', ...] + * } + * + * Or provide an array: + * ['module1', 'module2', ...] + * + * @type {Object|Array} + */ + BaseModule.SUB_MODULES = {}; + + /** + * All events of need to be listened. + * BaseModule will add/remove the event listener in start/stop functions. + * The function of '_handle_' form in this module will be invoked + * when the event is caught. + * @type {Array} + */ + BaseModule.EVENTS = []; + + /** + * All mozSettings need to be observed. + * BaseModule will observe and invoke the responsive + * this['_observe_' + key] function. + * @type {Array} + */ + BaseModule.SETTINGS = []; + + /** + * This defines a list of file path needs to be imported + * before the real start of this module. + * + * You can provide an object: + * Define a list of default type modules are able to be loaded in anytime. + * Define a list of device type modules can be loaded when running on a given + * device-type. + * { + * default: ['base_module1', 'base_module2', ...], + * device-type: ['module1', 'module2', ...] + * } + * + * Or provide an array: + * ['module1', 'module2', ...] + * + * @type {Object|Array} + */ + BaseModule.IMPORTS = {}; + + /** + * This tells System the sandbox what methods you are going to + * register and let the other to request. + * + * @example + * var MyModule = function() {}; + * MyModule.SERVICES = ['unlock']; + * MyModule.prototype = Object.create(BaseModule.prototype); + * MyModule.prototype.constructor = MyModule; + * MyModule.prototype.name = 'MyModule'; + * var m = new MyModule(); + * m.start(); + * // other module + * Service.request('MyModule:unlock').then(function(result) { + * }); + * Service.request('unlock').then(function(result) { + * // if the request is registered by only one module. + * }); + */ + BaseModule.SERVICES = []; + + /** + * The function or property exported here will be + * synchronously queried by other module in system app. + * If we are not started yet, they will get undefined. + * + * @example + * var MyModule = function() {}; + * MyModule.STATES = ['isActive']; + * MyModule.prototype = Object.create(BaseModule.prototype); + * MyModule.prototype.constructor = MyModule; + * MyModule.prototype.name = 'MyModule'; + * var m = new MyModule(); + * m.start(); + * // other module + * console.log(Service.query('MyModule.isActive')); + * // if the method name is unique. + * console.log(Service.query('isActive')); + * @type {Array} + */ + BaseModule.STATES = []; + + var SubmoduleMixin = { + loadWhenIdle: function(modules) { + return this.service.request('schedule', () => { + var submodules = this._arraylizeModules(this.constructor.SUB_MODULES); + var newModules = this._arraylizeModules(modules); + this.constructor.SUB_MODULES = submodules.concat(newModules); + return this._startSubModules(); + }); + }, + + _arraylizeModules: function(submodules) { + var modules; + var defaulModules; + var deviceModules; + + if (submodules && Array.isArray(submodules)) { + modules = submodules; + } else if (submodules && typeof submodules === 'object') { + defaulModules = submodules.default || []; + deviceModules = submodules[DEVICE_TYPE] || []; + modules = defaulModules.concat(deviceModules); + } else { + modules = []; + } + + return modules; + }, + + /** + * Helper function to load and start the submodules defined in + * |this.constructor.SUB_MODULES|. + */ + _startSubModules: function() { + var submodules = this._arraylizeModules(this.constructor.SUB_MODULES); + this.constructor.SUB_MODULES = submodules; + + if (submodules.length === 0) { + return Promise.resolve(); + } + + var unloaded = []; + submodules.forEach(function(submodule) { + if (BaseModule.defined(submodule) || window[submodule]) { + var name = BaseModule.lowerCapital(submodule); + if (!this[name]) { + this._initializeSubModule(name, submodule); + } + } else { + unloaded.push(submodule); + } + }, this); + + if (unloaded.length === 0) { + this.baseSubModuleLoaded && this.baseSubModuleLoaded(); + return; + } + + this.debug('lazy loading submodules: ' + + unloaded.concat()); + return BaseModule.lazyLoad(unloaded).then(() => { + this.debug('lazy loaded submodules: ' + + unloaded.concat()); + return Promise.all( + unloaded + .map(BaseModule.parsePath) + .map(function(module) { + var moduleName = BaseModule.lowerCapital(module.name); + if (!this[moduleName]) { + return this._initializeSubModule(moduleName, module.name); + } else { + return Promise.resolve(); + } + }, this)); + }); + }, + + _initializeSubModule: function(moduleName, module) { + var constructor = AVAILABLE_MODULES[module] || window[module]; + if (typeof(constructor) == 'function') { + this.debug('instantiating submodule: ' + moduleName); + this[moduleName] = new constructor(this); + // If there is a custom submodule loaded handler, call it. + // Otherwise we will start the submodule right away. + if (typeof(this['_' + moduleName + '_loaded']) == 'function') { + return this['_' + moduleName + '_loaded'](); + } else if (this.lifeCycleState !== 'stopped') { + return this[moduleName].start && this[moduleName].start(); + } + } else { + this[moduleName] = constructor; + // For the module which does not become class yet + if (this[moduleName] && this[moduleName].start) { + return this[moduleName].start(); + } else if (this[moduleName] && this[moduleName].init) { + // backward compatibility with init. + return this[moduleName].init(); + } else { + return undefined; + } + } + }, + + _stopSubModules: function() { + if (!this.constructor.SUB_MODULES) { + return; + } + this.constructor.SUB_MODULES.map(BaseModule.parsePath) + .forEach(function(module) { + var moduleName = BaseModule.lowerCapital(module.name); + if (this[moduleName]) { + this.debug('Stopping submodule: ' + moduleName); + this[moduleName].stop && this[moduleName].stop(); + } + }, this); + } + }; + + /** + * SettingsMixin will provide you the ability to watch + * and store the settings in this._settings + * @type {Object} + */ + var SettingMixin = { + observe: function(name, value) { + this.debug('observing ' + name + ' : ' + value); + this._settings[name] = value; + if (typeof(this['_observe_' + name]) == 'function') { + this.debug('observer for ' + name + ' found, invoking.'); + this['_observe_' + name](value); + } + }, + + _observeSettings: function() { + if (!this.constructor.SETTINGS) { + this.debug('No settings needed, skipping.'); + return; + } + this._settings = {}; + this.debug('~observing following settings: ' + + this.constructor.SETTINGS.concat()); + this.constructor.SETTINGS.forEach(function(setting) { + this.service.request('SettingsCore:addObserver', setting, this); + }, this); + }, + + _unobserveSettings: function() { + if (!this.constructor.SETTINGS) { + return; + } + this.constructor.SETTINGS.forEach(function(setting) { + this.service.request('SettingsCore:removeObserver', setting, this); + }, this); + } + }; + + var EventMixin = { + /** + * Custom global event handler before the event is handled + * by a specific handler. + * Override it if necessary. + */ + _pre_handleEvent: function() { + + }, + + /** + * Custom global event handler after the event is handled. + * Override it if necessary. + */ + _post_handleEvent: function() { + + }, + + _subscribeEvents: function() { + if (!this.constructor.EVENTS) { + this.debug('No events wanted, skipping.'); + return; + } + this.debug('event subcribing stage..'); + this.constructor.EVENTS.forEach(function(event) { + this.debug('subscribing ' + event); + window.addEventListener(event, this); + }, this); + }, + + _unsubscribeEvents: function() { + if (!this.constructor.EVENTS) { + return; + } + this.constructor.EVENTS.forEach(function(event) { + window.removeEventListener(event, this); + }, this); + }, + + handleEvent: function(evt) { + if (typeof(this._pre_handleEvent) == 'function') { + var shouldContinue = this._pre_handleEvent(evt); + if (shouldContinue === false) { + return; + } + } else { + this.debug('no handle event pre found. skip'); + } + if (typeof(this['_handle_' + evt.type]) == 'function') { + this.debug('handling ' + evt.type); + this['_handle_' + evt.type](evt); + } + if (typeof(this._post_handleEvent) == 'function') { + this._post_handleEvent(evt); + } + } + }; + + var ServiceMixin = { + _registerServices: function() { + if (!this.constructor.SERVICES) { + return; + } + this.constructor.SERVICES.forEach(function(service) { + this.service.register(service, this); + }, this); + }, + + _unregisterServices: function() { + if (!this.constructor.SERVICES) { + return; + } + this.constructor.SERVICES.forEach(function(service) { + this.service.unregister(service, this); + }, this); + } + }; + + var StateMixin = { + _registerStates: function() { + if (!this.constructor.STATES) { + return; + } + this.constructor.STATES.forEach(function(state) { + this.service.registerState(state, this); + }, this); + }, + + _unregisterStates: function() { + if (!this.constructor.STATES) { + return; + } + this.constructor.STATES.forEach(function(state) { + this.service.unregisterState(state, this); + }, this); + } + }; + + BaseModule.defined = function(name) { + return !!AVAILABLE_MODULES[name]; + }; + + BaseModule.__clearDefined = function() { + AVAILABLE_MODULES = []; + }; + + /** + * Mixin the prototype with give mixin object. + * @param {Object} prototype The prototype of a class + * @param {Object} mixin An object will be mixed into the prototype + */ + BaseModule.mixin = function (prototype, mixin) { + for (var prop in mixin) { + if (mixin.hasOwnProperty(prop)) { + prototype[prop] = mixin[prop]; + } + } + }; + + /** + * Create a module based on base module and give properties. + * The constructor will be placed in AVAILABLE_MODULES if you + * specify an unique name in the prototype. + * @example + * var MyModule = function() {}; + * BaseModule.create(MyModule, { + * name: 'MyModule' + * }); + * var myModule = BaseModule.instantiate('MyModule'); + * + * @param {Function} constructor The constructor function. + * @param {Object} prototype + * The prototype which will be injected into the class. + * @param {Object} properties + * The property object which includes getter/setter. + */ + BaseModule.create = function(constructor, prototype, properties) { + constructor.prototype = Object.create(BaseModule.prototype, properties); + constructor.prototype.constructor = constructor; + if (constructor.SETTINGS) { + BaseModule.mixin(constructor.prototype, SettingMixin); + } + if (constructor.EVENTS) { + BaseModule.mixin(constructor.prototype, EventMixin); + } + if (constructor.SERVICES) { + BaseModule.mixin(constructor.prototype, ServiceMixin); + } + if (constructor.STATES) { + BaseModule.mixin(constructor.prototype, StateMixin); + } + // Inject this anyway. + BaseModule.mixin(constructor.prototype, SubmoduleMixin); + if (prototype) { + BaseModule.mixin(constructor.prototype, prototype); + if (prototype.name) { + AVAILABLE_MODULES[prototype.name] = constructor; + } else { + console.warn('No name give, impossible to instantiate without name.'); + } + } + return constructor; + }; + + /** + * Create a new instance based on the module name given. + * It will look up |AVAILABLE_MODULES|. + * Note: this will instante multiple instances if called more than once. + * Also it's impossible to pass arguments now. + * @param {String} moduleName The module name + * which comes from the prototype of the module. + * @return {Object} Created instance. + */ + BaseModule.instantiate = function(moduleName) { + if (moduleName in AVAILABLE_MODULES) { + var args = Array.prototype.slice.call(arguments, 1); + var constructor = function() { + AVAILABLE_MODULES[moduleName].apply(this, args); + }; + constructor.prototype = AVAILABLE_MODULES[moduleName].prototype; + return new constructor(); + } + return undefined; + }; + + /** + * Lazy load an list of modules + * @param {Array} array A list of module names + * @return {Promise} The promise of lazy loading; + * it will be invoked once lazy loading is done. + */ + BaseModule.lazyLoad = function(array) { + var self = this; + return new Promise(function(resolve) { + var fileList = []; + array.forEach(function(module) { + fileList.push(BaseModule.object2fileName(module)); + }, self); + LazyLoader.load(fileList, function() { + resolve(); + }); + }); + }; + + /** + * A helper function to lowercase only the capital character. + * @example + * BaseModule.lowerCapital('AppWindowManager'); + * // appWindowManager + * @param {String} str String to be lowercased on capital + * @return {String} Captital lowerred string + */ + BaseModule.lowerCapital = function(str) { + return str.charAt(0).toLowerCase() + str.slice(1); + }; + + /** + * A helper function to split module expressions into "path" and "name". + * @example + * BaseModule.parsePath('path/to/ModuleName'); + * // {path: 'path/to/', name: 'ModuleName'} + * @param {String} str String to be splitted + * @return {Object} The result object with members: "path" and "name". + */ + BaseModule.parsePath = function(str) { + var [, path, name] = /^(.*\/|)(.+)$/.exec(str); + return { + path: path, + name: name + }; + }; + + /** + * A helper function to transform object name to file name + * @example + * BaseModule.object2fileName('AppWindowManager'); + * // 'js/app_window_manager.js' + * BaseModule.object2fileName('path/to/ModuleName'); + * // 'js/path/to/module_name.js' + * + * @param {String} string Module name + * @return {String} File name + */ + BaseModule.object2fileName = function(string) { + var i = 0; + var ch = ''; + + var module = BaseModule.parsePath(string); + var moduleName = module.name; + + while (i <= moduleName.length) { + var character = moduleName.charAt(i); + if (character !== character.toLowerCase()) { + if (ch === '') { + ch += character.toLowerCase(); + } else { + ch += '_' + character.toLowerCase(); + } + } else { + ch += character; + } + i++; + } + return '/js/' + module.path + ch + '.js'; + }; + + BaseModule.prototype = { + service: window.Service, + + DEBUG: false, + + TRACE: false, + + /** + * The name of this module which is usually the constructor function name + * and could be converted to the file name. + * For example, AppWindowManager will be mapped to app_window_manager.js + * This should be unique. + * @type {String} + */ + name: '(Anonymous)', + + EVENT_PREFIX: '', + + /** + * We are having three states: + * * starting + * * started + * * stopped + * @type {String} + */ + lifeCycleState: 'stopped', + + publish: function(event, detail, noPrefix) { + var prefix = noPrefix ? '' : this.EVENT_PREFIX; + var evt = new CustomEvent(prefix + event, + { + bubbles: true, + detail: detail || this + }); + + this.debug('publishing: ' + prefix + event); + + window.dispatchEvent(evt); + }, + + /** + * Basic log. + */ + debug: function() { + if (this.DEBUG || GLOBAL_DEBUG) { + console.log('[' + this.name + ']' + + '[' + this.service.currentTime() + '] ' + + Array.slice(arguments).concat()); + if (this.TRACE) { + console.trace(); + } + } else if (window.DUMP) { + DUMP('[' + this.name + ']' + + '[' + this.service.currentTime() + '] ' + + Array.slice(arguments).concat()); + } + }, + + /** + * Log some infomation. + */ + info: function() { + if (this.DEBUG || GLOBAL_DEBUG) { + console.info('[' + this.name + ']' + + '[' + this.service.currentTime() + '] ' + + Array.slice(arguments).concat()); + } + }, + + /** + * Log some warning message. + */ + warn: function() { + if (this.DEBUG || GLOBAL_DEBUG) { + console.warn('[' + this.name + ']' + + '[' + this.service.currentTime() + '] ' + + Array.slice(arguments).concat()); + } + }, + + /** + * Log some error message. + */ + error: function() { + if (this.DEBUG || GLOBAL_DEBUG) { + console.error('[' + this.name + ']' + + '[' + this.service.currentTime() + '] ' + + Array.slice(arguments).concat()); + } + }, + + writeSetting: function(settingObject) { + this.debug('writing ' + JSON.stringify(settingObject) + + ' to settings db'); + return this.service.request('SettingsCore:set', settingObject); + }, + + readSetting: function(name) { + if (this._settings && this._settings[name]) { + return Promise.resolve(this._settings[name]); + } else { + this.debug('reading ' + name + ' from settings db'); + return this.service.request('SettingsCore:get', name); + } + }, + + /** + * Custom start function, override it if necessary. + * If you want to block the start process of this module, + * return a promise here. + */ + _start: function() {}, + + /** + * Custom stop function. Override it if necessary. + */ + _stop: function() {}, + + /** + * The starting progress of a module has these steps: + * * import javascript files + * * lazy load submodules and instantiate once loaded. + * * custom start function + * * attach event listeners + * * observe settings + * * register services to System + * * DOM elements rendering (not implemented) + * The import is guranteed to happen before anything else. + * The service registration is expected to happen after everything is done. + * The ordering of the remaining parts should not depends each other. + * + * The start function will return a promise to let you know + * when the module is started. + * @example + * var a = BaseModule.instantiate('A'); + * a.start().then(() => { + * console.log('started'); + * }); + * + * Note: a module start promise will only be resolved + * after all the steps are resolved, including + * the custom start promise and the promises from all the submodules. + * So when you see a module is started, that means all of its + * submodules are started as well. + * + * @memberOf BaseModule.prototype + */ + start: function() { + if (this.lifeCycleState !== 'stopped') { + this.warn('already started'); + return Promise.reject('already started'); + } + this.switchLifeCycle('starting'); + return this.imports(); + }, + + __imported: function() { + // Do nothing if we are stopped. + if (this.lifeCycleState === 'stopped') { + this.warn('already stopped'); + return Promise.resolve(); + } + this.debug('in imported'); + return Promise.all([ + // Parent module needs to know the events from the submodule. + this._subscribeEvents && this._subscribeEvents(), + this._startSubModules && this._startSubModules(), + this._start(), + this._observeSettings && this._observeSettings(), + this._registerServices && this._registerServices(), + this._registerStates && this._registerStates()]).then(() => { + this.switchLifeCycle('started'); + }); + }, + + /** + * The stopping of a module has these steps: + * * unregister services to System sandbox + * * lazy load submodules and instantiate once loaded. + * * attach event listeners + * * observe settings + * * custom stop function + */ + stop: function() { + if (this.lifeCycleState === 'stopped') { + this.warn('already stopped'); + return; + } + this._unregisterServices && this._unregisterServices(); + this._unregisterStates && this._unregisterStates(); + this._stopSubModules && this._stopSubModules(); + this._unsubscribeEvents && this._unsubscribeEvents(); + this._unobserveSettings && this._unobserveSettings(); + this._stop(); + this.switchLifeCycle('stopped'); + }, + + switchLifeCycle: function(state) { + if (this.lifeCycleState === state) { + return; + } + + this.debug('life cycle state change: ' + + this.lifeCycleState + ' -> ' + state); + this.lifeCycleState = state; + this.publish(state); + }, + + imports: function() { + var imports = this._arraylizeModules(this.constructor.IMPORTS); + this.constructor.IMPORTS = imports; + + if (imports.length === 0) { + return this.__imported(); + } + + this.debug(imports); + this.debug('import loading.'); + return LazyLoader.load(imports) + .then(() => { + this.debug('imported..'); + return this.__imported(); + }); + } + }; + + exports.BaseModule = BaseModule; +}(window)); diff --git a/tv_apps/smart-system/js/bootstrap.js b/tv_apps/smart-system/js/bootstrap.js index 6ed7e2b8e119..39a5c421c568 100644 --- a/tv_apps/smart-system/js/bootstrap.js +++ b/tv_apps/smart-system/js/bootstrap.js @@ -8,7 +8,9 @@ SuspendingAppPriorityManager, TTLView, MediaRecording, AppWindowFactory, applications, LayoutManager, PermissionManager, Accessibility, - SleepMenu, InteractiveNotifications, ExternalStorageMonitor */ + SleepMenu, InteractiveNotifications, ExternalStorageMonitor, + BaseModule */ + 'use strict'; @@ -136,6 +138,9 @@ window.addEventListener('load', function startup() { { bubbles: true, cancelable: false, detail: { type: 'system-message-listener-ready' } }); window.dispatchEvent(evt); + + window.core = BaseModule.instantiate('Core'); + window.core && window.core.start(); }); window.usbStorage = new UsbStorage(); diff --git a/tv_apps/smart-system/js/core.js b/tv_apps/smart-system/js/core.js new file mode 100644 index 000000000000..404defd79d59 --- /dev/null +++ b/tv_apps/smart-system/js/core.js @@ -0,0 +1,70 @@ +/* global BaseModule */ +'use strict'; + +(function(exports) { + /** + * This is the bootstrap module of the system app. + * It is responsible to instantiate and start the other core modules + * and sub systems per API. + */ + var Core = function() { + }; + + Core.SERVICES = [ + 'getAPI' + ]; + + BaseModule.create(Core, { + name: 'Core', + + REGISTRY: { + 'mozSettings': 'SettingsCore' + }, + + getAPI: function(api) { + for (var key in this.REGISTRY) { + if (api === BaseModule.lowerCapital(key.replace('moz', ''))) { + return navigator[key]; + } + } + return false; + }, + + _start: function() { + for (var api in this.REGISTRY) { + this.debug('Detecting API: ' + api + + ' and corresponding module: ' + this.REGISTRY[api]); + if (navigator[api]) { + this.debug('API: ' + api + ' found, starting the handler.'); + this.startAPIHandler(api, this.REGISTRY[api]); + } else { + this.debug('API: ' + api + ' not found, skpping the handler.'); + } + } + }, + + startAPIHandler: function(api, handler) { + return new Promise(function(resolve, reject) { + BaseModule.lazyLoad([handler]).then(function() { + var moduleName = BaseModule.lowerCapital(handler); + this[moduleName] = + BaseModule.instantiate(handler, navigator[api], this); + if (!this[moduleName]) { + reject(); + } + this[moduleName].start && this[moduleName].start(); + resolve(); + }.bind(this)); + }.bind(this)); + }, + + _stop: function() { + for (var api in this.REGISTRY) { + var moduleName = + this.REGISTRY[api].charAt(0).toUpperCase() + + this.REGISTRY[api].slice(1); + this[moduleName] && this[moduleName].stop(); + } + } + }); +}(window)); diff --git a/tv_apps/smart-system/js/service.js b/tv_apps/smart-system/js/service.js index 909cbf0d1118..f8351f90700b 100644 --- a/tv_apps/smart-system/js/service.js +++ b/tv_apps/smart-system/js/service.js @@ -8,14 +8,229 @@ * @module Service */ exports.Service = { + /** + * Stores the servers by the server name. + * @type {Map} + */ + _providers: new Map(), + + /** + * Stores the services by the services name. + * @type {Map} + */ + _services: new Map(), + + /** + * Stores the awaiting consumers by the service name. + * @type {Map} + */ + _requestsByService: new Map(), + + /** + * Stores the awaiting consumers by the server name. + * @type {Map} + */ + _requestsByProvider: new Map(), + + /** + * Request a service to System and get a promise. + * The service name may include the name of server or not if it is unique. + * @example + * Service.request('locked').then(function() {}); + * Service.request('addObserver', 'test.enabled', this).then(function() {}); + * Service.request('StatusBar:height').then(function() {}); + * + * @param {String} service Service name + * @return {Promise} + */ + request: function(service) { + var requestItems = service.split(':'); + var args = Array.prototype.slice.call(arguments, 1); + var self = this; + this.debug(requestItems); + if (requestItems.length > 1) { + var serverName = requestItems[0]; + var serviceName = requestItems[1]; + if (this._providers.get(serverName)) { + this.debug('service: ' + serviceName + + ' is online, perform the request with ' + args.concat()); + return new Promise(function(resolve, reject) { + resolve(self._providers.get(serverName)[serviceName].apply( + self._providers.get(serverName), args)); + }); + } else { + return new Promise(function(resolve, reject) { + self.debug('service: ' + service + ' is offline, queue the task.'); + if (!self._requestsByProvider.has(serverName)) { + self._requestsByProvider.set(serverName, []); + } + self._requestsByProvider.get(serverName).push({ + service: serviceName, + resolve: resolve, + args: args + }); + }); + } + return; + } + + if (this._services.has(service)) { + var server = this._services.get(service); + this.debug('service [' + service + + '] provider [' + server.name + '] is online, perform the task.'); + return new Promise(function(resolve, reject) { + resolve(server[service].apply(server, args)); + }); + } else { + this.debug('service: ' + service + ' is offline, queue the task.'); + var promise = new Promise(function(resolve) { + self.debug('storing the requests...'); + if (!self._requestsByService.has(service)) { + self._requestsByService.set(service, []); + } + self._requestsByService.get(service).push({ + service: service, + args: args, + resolve: resolve + }); + }); + return promise; + } + }, + + /** + * Register an asynchronous service to Service. + * If there is any client awaiting this service, they will be executed after + * registration. + * @param {String} service Service name + * @param {Object} server The server object which has the service. + */ + register: function(service, server) { + var self = this; + if (!this._providers.has(server.name)) { + this._providers.set(server.name, server); + } + this.debug((server.name || '(Anonymous)') + + ' is registering service: [' + service + ']'); + + this.debug('checking awaiting requests by server..'); + if (this._requestsByProvider.has(server.name)) { + this._requestsByProvider.get(server.name).forEach(function(request) { + self.debug('resolving..', server, + server.name, request.service, request.args); + var result = (typeof(server[request.service]) === 'function') ? + server[request.service].apply(server, request.args) : + server[request.service]; + + request.resolve(result); + }); + this._requestsByProvider.delete(server.name); + } + + if (!this._services.has(service)) { + this._services.set(service, server); + } else { + console.warn('the service [' + service + '] has already been ' + + 'registered by other server.'); + return; + } + + this.debug('checking awaiting requests by service..'); + if (this._requestsByService.has(service)) { + this._requestsByService.get(service).forEach(function(request) { + self.debug('resolving..', server, request.service); + request.resolve(server[request.service].apply(server, request.args)); + }); + this._requestsByService.delete(service); + } + }, + + /** + * Unregister an asynchronous service to System + * @param {String} service The name of the service + * @param {Object} server The server + */ + unregister: function(service, server) { + this._providers.delete(server.name); + var se = this._services.get(service); + if (se && server === se) { + this._services.delete(service); + } + }, + + _states: new Map(), + _statesByState: new Map(), + + registerState: function(state, provider) { + this._states.set(provider.name, provider); + this._statesByState.set(state, provider); + }, + + unregisterState: function(state, provider) { + this._states.delete(provider.name); + var machine = this._statesByState.get(state); + if (machine === provider) { + this._statesByState.delete(state); + } + }, + + /** + * Synchonously query the state of specific state machine. + * If the state machine is not started, + * you will get undefined. + * + * @example + * Service.query('FtuLauncher.isFtuRunning'); + * Service.query('isFtuRunning'); + * + * @param {String} state The machine name and the state name. + * @return {String|Boolean|Number} + */ + query: function(stateString) { + this.debug(stateString); + var args = stateString.split('.'); + var state, provider; + if (args.length > 1) { + provider = this._states.get(args[0]); + state = args[1]; + } else { + state = args[0]; + provider = this._statesByState.get(state); + } + if (!provider) { + this.debug('Provider not ready, return undefined state.'); + return undefined; + } + if (typeof(provider[state]) === 'function') { + return provider[state](); + } else { + return provider[state]; + } + }, + + /** + * XXX: applications should register a service + * for ready check by Service.register('ready', applications). + */ + get applicationReady() { + return window.applications && window.applications.ready; + }, + + /** * Indicates the system is busy doing something. * Now it stands for the foreground app is not loaded yet. + * + * XXX: AppWindowManager should register a service + * for isBusyLoading query by + * Service.register('isBusyLoading', appWindowManager). */ + isBusyLoading: function() { var app = window.AppWindowManager.getActiveApp(); return app && !app.loaded; }, + /** * Record the start time of the system for later debugging usage. * @access private @@ -64,6 +279,9 @@ window.dispatchEvent(evt); }, + /** + * XXX: FtuLauncher should register 'isFtuRunning' service. + */ get runningFTU() { if ('undefined' === typeof window.FtuLauncher) { return false; @@ -72,6 +290,9 @@ } }, + /** + * XXX: LockscreenWindowManager should register 'locked' service. + */ get locked() { return false; }, diff --git a/tv_apps/smart-system/js/settings_core.js b/tv_apps/smart-system/js/settings_core.js new file mode 100644 index 000000000000..5e4168cab113 --- /dev/null +++ b/tv_apps/smart-system/js/settings_core.js @@ -0,0 +1,157 @@ +/* exported SettingsCore */ +/* global BaseModule */ +'use strict'; + +(function(exports) { + var SettingsCore = function() { + this.settings = window.navigator.mozSettings; + }; + SettingsCore.SERVICES = [ + 'get', + 'set', + 'addObserver', + 'removeObserver' + ]; + /** + * SettingsCore is a wrapper to access navigator.mozSettings + * and provides some API it doesn't provide. + * *SettingsCore#addObserver(SETTING_NAME, OBSERVER_OBJECT) + * *SettingsCore#set(SETTING_OBJECT) + * *SettingsCore#get(SETTING_NAME) + * @class SettingsCore + */ + BaseModule.create(SettingsCore, { + name: 'SettingsCore', + + /* lock stores here */ + _lock: null, + + /* keep record of observers in order to remove them in the future */ + _observers: [], + + /** + * getSettingsLock: create a lock or retrieve one that we saved. + * mozSettings.createLock() is expensive and lock should be reused + * whenever possible. + * @memberOf SettingsCore.prototype + */ + getSettingsLock: function sl_getSettingsLock() { + // If there is a lock present we return that + if (this._lock && !this._lock.closed) { + return this._lock; + } + + return (this._lock = this.settings.createLock()); + }, + + get: function(name) { + var self = this; + return new Promise(function(resolve, reject) { + self.debug('reading ' + name + ' from settings db.'); + var get = self.getSettingsLock().get(name); + get.addEventListener('success', (function() { + self.debug('...value is ' + get.result[name]); + resolve(get.result[name]); + })); + get.addEventListener('error', (function() { + reject(); + })); + }); + }, + + set: function(notifier) { + var self = this; + return new Promise(function(resolve, reject) { + self.debug('writing ' + JSON.stringify(notifier) + ' to settings db.'); + var lock = self.getSettingsLock(); + var set = lock.set(notifier); + set.addEventListener('success', function() { + resolve(); + }); + set.addEventListener('error', function() { + reject(); + }); + }); + }, + + /** + * addObserver provides a "addEventListener"-like interface + * to observe settings change. + * + * @example + * var s = new SettingsCore(); + * s.start(); + * var MyModule = { + * init: function() { + * s.addObserver('lockscreen.enabled', this); + * s.addObserver('lockscreen.locked', this); + * }, + * observe: function(name, value) { + * console.log('settings of ' + name + ' had changed to ' + value); + * } + * }; + * MyModule.init(); + * + * @param {String} name The settings name + * @param {Object} context The object which wants to observe the settings. + * It should have a method named for 'observe'. + */ + addObserver: function(name, context) { + this.debug('adding observer for ' + context + ' on ' + name); + if (context) { + if (!this.settings) { + window.setTimeout(function() { + if ('observe' in context) { + context.observe.call(context, name, null); + } else if (typeof(context) === 'function') { + context(null); + } + }); + return; + } + var self = this; + var req = this.getSettingsLock().get(name); + + req.addEventListener('success', (function onsuccess() { + self.debug('get settings ' + name + ' as ' + req.result[name]); + self.debug('now performing the observer in ' + context.name); + if ('observe' in context) { + context.observe.call(context, name, req.result[name]); + } else if (typeof(context) === 'function') { + context(req.result[name]); + } + })); + + var settingChanged = function settingChanged(evt) { + self.debug('observing settings ' + evt.settingName + + ' changed to ' + evt.settingValue); + self.debug('now performing the observer in ' + context.name); + if ('observe' in context) { + context.observe.call(context, evt.settingName, evt.settingValue); + } else if (typeof(context) === 'function') { + context(evt.settingValue); + } + }; + this.settings.addObserver(name, settingChanged); + this._observers.push({ + name: name, + context: context, + observer: settingChanged + }); + } else { + this.warn('irregular observer ' + context.name + ', stop oberseving'); + } + }, + + removeObserver: function(name, context) { + var settings = this.settings; + var that = this; + this._observers.forEach(function(value, index) { + if (value.name === name && value.context === context) { + settings.removeObserver(name, value.observer); + that._observers.splice(index, 1); + } + }); + } + }); +}(window)); diff --git a/tv_apps/smart-system/test/unit/base_module_test.js b/tv_apps/smart-system/test/unit/base_module_test.js new file mode 100644 index 000000000000..40604de0dd98 --- /dev/null +++ b/tv_apps/smart-system/test/unit/base_module_test.js @@ -0,0 +1,747 @@ +/* global BaseModule, MocksHelper, MockService, MockLazyLoader, MockPromise, + Deferred */ +'use strict'; + +require('/shared/test/unit/mocks/mock_service.js'); +require('/shared/test/unit/mocks/mock_promise.js'); +require('/shared/test/unit/mocks/mock_lazy_loader.js'); +requireApp('smart-system/test/unit/deferred.js'); + +var mocksForBaseModule = new MocksHelper([ + 'LazyLoader', 'Service' +]).init(); + +suite('smart-system/BaseModule', function() { + mocksForBaseModule.attachTestHelpers(); + + setup(function(done) { + requireApp('smart-system/js/base_module.js', done); + }); + + test('lowercase capital', function() { + assert.equal('appWindowManager', + BaseModule.lowerCapital('AppWindowManager')); + }); + + test('parse module path', function() { + var module = BaseModule.parsePath('path/to/ModuleName'); + assert.equal(module.path, 'path/to/'); + assert.equal(module.name, 'ModuleName'); + }); + + test('object name to file name', function() { + assert.deepEqual('/js/app_window_manager.js', + BaseModule.object2fileName('AppWindowManager')); + assert.deepEqual('/js/path/to/module_name.js', + BaseModule.object2fileName('path/to/ModuleName')); + }); + + test('lazy load from an array of submodule strings', function(done) { + var stubLoad = this.sinon.stub(MockLazyLoader, 'load'); + BaseModule.lazyLoad(['AppWindowManager']).then(function() { + done(); + }); + assert.isTrue(stubLoad.calledWith(['/js/app_window_manager.js'])); + stubLoad.yield(); + }); + + suite('Launching promise', function() { + test('start should resolve right away', function(done) { + var LaunchingPromiseTester = function() {}; + BaseModule.create(LaunchingPromiseTester, { + name: 'LaunchingPromiseTester' + }); + var lpt = BaseModule.instantiate('LaunchingPromiseTester'); + lpt.start().catch((e) => {throw e || 'Should not throw'; }) + .then(done, done); + }); + + test('custom start', function(done) { + var LaunchingPromiseTester = function() {}; + BaseModule.create(LaunchingPromiseTester, { + name: 'LaunchingPromiseTester', + _start: function() { + return new Promise(function(resolve) { + resolve(); + }); + } + }); + var lpt = BaseModule.instantiate('LaunchingPromiseTester'); + lpt.start().catch((e) => {throw e || 'Should not throw'; }) + .then(done, done); + }); + }); + + suite('BaseModule.instantiate', function() { + test('Get a new instance if the module is available', function() { + var InstantiationTester = function() {}; + BaseModule.create(InstantiationTester, { + name: 'InstantiationTester' + }); + + assert.isDefined(BaseModule.instantiate('InstantiationTester')); + assert.equal(BaseModule.instantiate('InstantiationTester').name, + 'InstantiationTester'); + }); + + test('Get undefined if the module is unavailable', function() { + assert.isUndefined(BaseModule.instantiate('NoSuchModule')); + }); + }); + + suite('Centralized settins observer', function() { + var fakeAppWindowManager, settings; + setup(function() { + var FakeAppWindowManager = function() {}; + FakeAppWindowManager.SETTINGS = ['app-suspending.enabled']; + BaseModule.create(FakeAppWindowManager, { + name: 'FakeAppWindowManager' + }); + settings = { + 'app-suspending.enabled': true + }; + fakeAppWindowManager = new FakeAppWindowManager(); + }); + + teardown(function() { + fakeAppWindowManager.stop(); + }); + + test('addObserver will be called if SETTINGS is specified', function(done) { + var spy = this.sinon.stub(fakeAppWindowManager, 'observe'); + this.sinon.stub(MockService, 'request', + function(service, name, consumer) { + if (service === 'SettingsCore:addObserver') { + assert.equal(name, 'app-suspending.enabled'); + assert.deepEqual(consumer, fakeAppWindowManager); + consumer.observe(name, settings[name]); + assert.isTrue(spy.calledWith('app-suspending.enabled', true)); + done(); + } + }); + + fakeAppWindowManager.start(); + }); + + test('get settings', function(done) { + this.sinon.stub(MockService, 'request').returns( + new Promise(function(resolve) { + resolve(new Promise(function(iresolve) { + iresolve(true); + })); + })); + + fakeAppWindowManager.readSetting('app-suspending.enabled') + .then(function(value) { + assert.equal(value, true); + done(); + }); + }); + + test('set settings', function(done) { + this.sinon.stub(MockService, 'request', + function(service, object) { + for (var key in object) { + settings[key] = object[key]; + } + assert.equal(settings['app-suspending.enabled'], false); + done(); + }); + + fakeAppWindowManager.writeSetting({'app-suspending.enabled': false}); + }); + + test('settings relation function will not be injected if not specified', + function() { + var fakeAppWindowManager2; + var FakeAppWindowManager2 = function() {}; + BaseModule.create(FakeAppWindowManager2, { + name: 'FakeAppWindowManager2' + }); + fakeAppWindowManager2 = new FakeAppWindowManager2(); + assert.isUndefined(fakeAppWindowManager2.observe); + assert.isUndefined(fakeAppWindowManager2._observeSettings); + assert.isUndefined(fakeAppWindowManager2._unobserveSettings); + }); + }); + + suite('Centralized event handler', function() { + var fakeAppWindowManager; + setup(function() { + var FakeAppWindowManager = function() {}; + FakeAppWindowManager.EVENTS = ['ftuskip']; + BaseModule.create(FakeAppWindowManager, { + name: 'FakeAppWindowManager' + }); + fakeAppWindowManager = new FakeAppWindowManager(); + }); + teardown(function() { + fakeAppWindowManager.stop(); + }); + + test('handleEvent should be called when registering events', function() { + var stubHandleEvent = + this.sinon.stub(fakeAppWindowManager, 'handleEvent'); + window.dispatchEvent(new CustomEvent('ftuskip')); + assert.isFalse(stubHandleEvent.called); + fakeAppWindowManager.start(); + var ftuskipEvent = new CustomEvent('ftuskip'); + window.dispatchEvent(ftuskipEvent); + assert.isTrue(stubHandleEvent.calledWith(ftuskipEvent)); + }); + + test('specific handler should be called if it exists', function() { + var stubHandleFtuskip = + fakeAppWindowManager._handle_ftuskip = this.sinon.spy(); + window.dispatchEvent(new CustomEvent('ftuskip')); + assert.isFalse(stubHandleFtuskip.called); + fakeAppWindowManager.start(); + var ftuskipEvent = new CustomEvent('ftuskip'); + window.dispatchEvent(ftuskipEvent); + assert.isTrue(stubHandleFtuskip.calledWith(ftuskipEvent)); + }); + + test('pre handler should be called if it exists', function() { + var spy = fakeAppWindowManager._handle_ftuskip = this.sinon.spy(); + var ftuskipEvent = new CustomEvent('ftuskip'); + window.dispatchEvent(ftuskipEvent); + assert.isFalse(spy.called); + fakeAppWindowManager.start(); + window.dispatchEvent(ftuskipEvent); + assert.isTrue(spy.calledWith(ftuskipEvent)); + }); + + test('pre handler should be called if it exists', function() { + var spy = fakeAppWindowManager._pre_handleEvent = this.sinon.spy(); + var spyFtuskip = fakeAppWindowManager._handle_ftuskip = this.sinon.spy(); + var ftuskipEvent = new CustomEvent('ftuskip'); + window.dispatchEvent(ftuskipEvent); + assert.isFalse(spy.called); + assert.isFalse(spyFtuskip.called); + fakeAppWindowManager.start(); + window.dispatchEvent(ftuskipEvent); + assert.isTrue(spy.calledWith(ftuskipEvent)); + assert.isTrue(spyFtuskip.calledWith(ftuskipEvent)); + }); + + test('specific event handler should be not called ' + + 'if pre handler returns false', function() { + fakeAppWindowManager._pre_handleEvent = function() {}; + var spy = this.sinon.stub(fakeAppWindowManager, + '_pre_handleEvent').returns(false); + var spyFtuskip = fakeAppWindowManager._handle_ftuskip = this.sinon.spy(); + var ftuskipEvent = new CustomEvent('ftuskip'); + + window.dispatchEvent(ftuskipEvent); + assert.isFalse(spy.called); + assert.isFalse(spyFtuskip.called); + fakeAppWindowManager.start(); + window.dispatchEvent(ftuskipEvent); + assert.isTrue(spy.calledWith(ftuskipEvent)); + assert.isFalse(spyFtuskip.calledWith(ftuskipEvent)); + }); + + test('post handler should be called if it exists', function() { + var spy = fakeAppWindowManager._post_handleEvent = this.sinon.spy(); + var ftuskipEvent = new CustomEvent('ftuskip'); + window.dispatchEvent(ftuskipEvent); + assert.isFalse(spy.called); + fakeAppWindowManager.start(); + window.dispatchEvent(ftuskipEvent); + assert.isTrue(spy.calledWith(ftuskipEvent)); + }); + + test('event relation function will not be injected if not specified', + function() { + var FakeAppWindowManager2 = function() {}; + BaseModule.create(FakeAppWindowManager2, { + name: 'FakeAppWindowManager2' + }); + var fakeAppWindowManager2 = new FakeAppWindowManager2(); + assert.isUndefined(fakeAppWindowManager2.handleEvent); + assert.isUndefined(fakeAppWindowManager2._pre_handleEvent); + assert.isUndefined(fakeAppWindowManager2._post_handleEvent); + assert.isUndefined(fakeAppWindowManager2._subscribeEvents); + assert.isUndefined(fakeAppWindowManager2._unsubscribeEvents); + }); + }); + + suite('Submodule management', function() { + var fakeAppWindowManager, fakePromise; + setup(function() { + BaseModule.__clearDefined(); + }); + suite('Already defined submodules with array SUB_MODULES', function() { + setup(function() { + fakePromise = new MockPromise(); + this.sinon.stub(BaseModule, 'lazyLoad', function() { + return fakePromise; + }); + window.FakeAppWindowManager = function() {}; + window.FakeAppWindowManager.SUB_MODULES = ['FakeTaskManager']; + BaseModule.create(window.FakeAppWindowManager, { + name: 'FakeAppWindowManager' + }); + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + fakeAppWindowManager = new window.FakeAppWindowManager(); + fakeAppWindowManager.start(); + }); + + test('submodule should be not loaded', function() { + fakeAppWindowManager._fakeTaskManager_loaded = this.sinon.spy(); + assert.isDefined(fakeAppWindowManager.fakeTaskManager); + assert.isFalse(fakeAppWindowManager._fakeTaskManager_loaded.called); + }); + }); + suite('Already defined submodules with object SUB_MODULES', function() { + setup(function() { + fakePromise = new MockPromise(); + this.sinon.stub(BaseModule, 'lazyLoad', function() { + return fakePromise; + }); + window.FakeAppWindowManager = function() {}; + window.FakeAppWindowManager.SUB_MODULES = { + default: ['FakeTaskManager'] + }; + BaseModule.create(window.FakeAppWindowManager, { + name: 'FakeAppWindowManager' + }); + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + fakeAppWindowManager = new window.FakeAppWindowManager(); + fakeAppWindowManager.start(); + }); + + test('submodule should be not loaded', function() { + fakeAppWindowManager._fakeTaskManager_loaded = this.sinon.spy(); + assert.isDefined(fakeAppWindowManager.fakeTaskManager); + assert.isFalse(fakeAppWindowManager._fakeTaskManager_loaded.called); + }); + }); + suite('Not defined submodules with array SUB_MODULES', function() { + setup(function() { + fakePromise = new MockPromise(); + this.sinon.stub(BaseModule, 'lazyLoad', function() { + return fakePromise; + }); + window.FakeAppWindowManager = function() {}; + window.FakeAppWindowManager.SUB_MODULES = [ + 'FakeTaskManager', + 'path/to/FakeSubModuleInDir' + ]; + BaseModule.create(window.FakeAppWindowManager, { + name: 'FakeAppWindowManager' + }); + window.FakeTaskManager = null; + window.FakeSubModuleInDir = null; + fakeAppWindowManager = new window.FakeAppWindowManager(); + fakeAppWindowManager.start(); + }); + + teardown(function() { + fakeAppWindowManager.stop(); + window.FakeTaskManager = null; + window.FakeAppWindowManager = null; + window.FakeSubModuleInDir = null; + }); + + test('submodule should be loaded', function() { + var spy = fakeAppWindowManager._fakeTaskManager_loaded = + this.sinon.spy(); + var spy2 = fakeAppWindowManager._fakeSubModuleInDir_loaded = + this.sinon.spy(); + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + window.FakeSubModuleInDir = function() {}; + BaseModule.create(window.FakeSubModuleInDir, { + name: 'FakeSubModuleInDir' + }); + fakePromise.mFulfillToValue(); + assert.isDefined(fakeAppWindowManager.fakeTaskManager); + assert.isTrue(spy.called); + assert.isDefined(fakeAppWindowManager.fakeSubModuleInDir); + assert.isTrue(spy2.called); + }); + + test('submodule loaded handler should be called if it exists', + function() { + var spy = fakeAppWindowManager._fakeTaskManager_loaded = + this.sinon.spy(); + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + fakePromise.mFulfillToValue(); + assert.isDefined(fakeAppWindowManager.fakeTaskManager); + assert.isTrue(spy.called); + }); + + test('submodule should be started once parent is started', function() { + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + var spyStart = this.sinon.stub(window.FakeTaskManager.prototype, + 'start'); + fakePromise.mFulfillToValue(); + assert.isTrue(spyStart.called); + }); + + test('submodule should be stopped once parent is stopped', function() { + window.FakeTaskManager = function() {}; + window.FakeSubModuleInDir = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + BaseModule.create(window.FakeSubModuleInDir, { + name: 'FakeSubModuleInDir' + }); + fakePromise.mFulfillToValue(); + var spyStop = + this.sinon.stub(fakeAppWindowManager.fakeTaskManager, 'stop'); + var spyStop2 = + this.sinon.stub(fakeAppWindowManager.fakeSubModuleInDir, 'stop'); + + fakeAppWindowManager.stop(); + assert.isTrue(spyStop.called); + assert.isTrue(spyStop2.called); + }); + + test('submodule should not be started if the parent is already stopped', + function() { + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + var spyStart = + this.sinon.stub(window.FakeTaskManager.prototype, 'start'); + fakeAppWindowManager.stop(); + + fakePromise.mFulfillToValue(); + assert.isFalse(spyStart.called); + }); + }); + + suite('Not defined submodules with object SUB_MODULES', function() { + setup(function() { + fakePromise = new MockPromise(); + this.sinon.stub(BaseModule, 'lazyLoad', function() { + return fakePromise; + }); + window.FakeAppWindowManager = function() {}; + window.FakeAppWindowManager.SUB_MODULES = { + default: ['FakeTaskManager'], + _GAIA_DEVICE_TYPE_: ['path/to/FakeSubModuleInDir'] + }; + BaseModule.create(window.FakeAppWindowManager, { + name: 'FakeAppWindowManager' + }); + window.FakeTaskManager = null; + window.FakeSubModuleInDir = null; + fakeAppWindowManager = new window.FakeAppWindowManager(); + fakeAppWindowManager.start(); + }); + + teardown(function() { + fakeAppWindowManager.stop(); + window.FakeTaskManager = null; + window.FakeAppWindowManager = null; + window.FakeSubModuleInDir = null; + }); + + test('submodule should be loaded', function() { + var spy = fakeAppWindowManager._fakeTaskManager_loaded = + this.sinon.spy(); + var spy2 = fakeAppWindowManager._fakeSubModuleInDir_loaded = + this.sinon.spy(); + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + window.FakeSubModuleInDir = function() {}; + BaseModule.create(window.FakeSubModuleInDir, { + name: 'FakeSubModuleInDir' + }); + fakePromise.mFulfillToValue(); + assert.isDefined(fakeAppWindowManager.fakeTaskManager); + assert.isTrue(spy.called); + assert.isDefined(fakeAppWindowManager.fakeSubModuleInDir); + assert.isTrue(spy2.called); + }); + + test('submodule loaded handler should be called if it exists', + function() { + var spy = fakeAppWindowManager._fakeTaskManager_loaded = + this.sinon.spy(); + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + fakePromise.mFulfillToValue(); + assert.isDefined(fakeAppWindowManager.fakeTaskManager); + assert.isTrue(spy.called); + }); + + test('submodule should be started once parent is started', function() { + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + var spyStart = this.sinon.stub(window.FakeTaskManager.prototype, + 'start'); + fakePromise.mFulfillToValue(); + assert.isTrue(spyStart.called); + }); + + test('submodule should be stopped once parent is stopped', function() { + window.FakeTaskManager = function() {}; + window.FakeSubModuleInDir = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + BaseModule.create(window.FakeSubModuleInDir, { + name: 'FakeSubModuleInDir' + }); + fakePromise.mFulfillToValue(); + var spyStop = + this.sinon.stub(fakeAppWindowManager.fakeTaskManager, 'stop'); + var spyStop2 = + this.sinon.stub(fakeAppWindowManager.fakeSubModuleInDir, 'stop'); + + fakeAppWindowManager.stop(); + assert.isTrue(spyStop.called); + assert.isTrue(spyStop2.called); + }); + + test('submodule should not be started if the parent is already stopped', + function() { + window.FakeTaskManager = function() {}; + BaseModule.create(window.FakeTaskManager, { + name: 'FakeTaskManager' + }); + var spyStart = + this.sinon.stub(window.FakeTaskManager.prototype, 'start'); + fakeAppWindowManager.stop(); + + fakePromise.mFulfillToValue(); + assert.isFalse(spyStart.called); + }); + }); + }); + + suite('Service registration', function() { + var fakeAppWindowManager; + setup(function() { + var FakeAppWindowManager = function() {}; + FakeAppWindowManager.SERVICES = ['isBusyLoading']; + BaseModule.create(FakeAppWindowManager, { + name: 'FakeAppWindowManager', + isBusyLoading: function() {} + }); + fakeAppWindowManager = new FakeAppWindowManager(); + fakeAppWindowManager.start(); + }); + teardown(function() { + fakeAppWindowManager.stop(); + }); + + test('register/unregister functions should be injected', function() { + assert.isDefined(fakeAppWindowManager._registerServices); + assert.isDefined(fakeAppWindowManager._unregisterServices); + }); + + test('Should register service to System when starting', function() { + fakeAppWindowManager.stop(); + var stubRegister = this.sinon.stub(MockService, 'register'); + fakeAppWindowManager.start(); + assert.isTrue(stubRegister.calledWith('isBusyLoading', + fakeAppWindowManager)); + }); + + test('Should unregister service to System when stopping', function() { + var stubRegister = this.sinon.stub(MockService, 'unregister'); + fakeAppWindowManager.stop(); + assert.isTrue(stubRegister.calledWith('isBusyLoading', + fakeAppWindowManager)); + }); + + test('Should not inject service registration functions', function() { + var FakeAppWindowManager2 = function() {}; + BaseModule.create(FakeAppWindowManager2, { + name: 'FakeAppWindowManager2', + isBusyLoading: function() {} + }); + var fakeAppWindowManager2 = new FakeAppWindowManager2(); + assert.isUndefined(fakeAppWindowManager2._registerServices); + assert.isUndefined(fakeAppWindowManager2._unregisterServices); + }); + }); + + suite('State registration', function() { + var fakeFtuLauncher; + setup(function() { + var FakeFtuLauncher = function() {}; + FakeFtuLauncher.STATES = ['isUpgrading']; + BaseModule.create(FakeFtuLauncher, { + _upgrading: false, + name: 'FakeFtuLauncher', + isUpgrading: function() { + return this._upgrading; + } + }); + fakeFtuLauncher = new FakeFtuLauncher(); + fakeFtuLauncher.start(); + }); + + teardown(function() { + fakeFtuLauncher.stop(); + }); + + test('Should register service to System when starting', function() { + fakeFtuLauncher.stop(); + var stubRegister = this.sinon.stub(MockService, 'registerState'); + fakeFtuLauncher.start(); + assert.isTrue(stubRegister.calledWith('isUpgrading', + fakeFtuLauncher)); + }); + + test('Should unregister service to System when stopping', function() { + var stubRegister = this.sinon.stub(MockService, 'unregisterState'); + fakeFtuLauncher.stop(); + assert.isTrue(stubRegister.calledWith('isUpgrading', + fakeFtuLauncher)); + }); + + test('Should not inject service registration functions', function() { + var FakeFtuLauncher2 = function() {}; + BaseModule.create(FakeFtuLauncher2, { + name: 'FakeFtuLauncher2', + isUpgrading: function() {} + }); + var fakeFtuLauncher2 = new FakeFtuLauncher2(); + this.sinon.stub(MockService, 'registerState'); + this.sinon.stub(MockService, 'unregisterState'); + fakeFtuLauncher2.start(); + assert.isFalse(MockService.registerState.called); + fakeFtuLauncher2.stop(); + assert.isFalse(MockService.unregisterState.called); + }); + }); + + suite('Start chain', function() { + var chainTester; + var chainTester_startDeferred, cModule_startDeferred; + setup(function() { + chainTester_startDeferred = new Deferred(); + cModule_startDeferred = new Deferred(); + var ChainTester = function() {}; + var CModule = function() {}; + BaseModule.create(CModule, { + name: 'CModule', + DEBUG: true, + _start: sinon.spy(function() { + return cModule_startDeferred.promise; + }) + }); + ChainTester.SUB_MODULES = ['CModule']; + BaseModule.create(ChainTester, { + name: 'ChainTester', + DEBUG: true, + _start: sinon.spy(function() { + return chainTester_startDeferred.promise; + }) + }); + chainTester = new ChainTester(); + }); + test('Should wait until child start', function(done) { + var p = chainTester.start(); + assert.isTrue(chainTester._start.calledOnce); + assert.isTrue(chainTester.cModule._start.calledOnce); + p.then(done, done); + chainTester_startDeferred.resolve(); + cModule_startDeferred.resolve(); + }); + }); + + suite('Import', function() { + var importTester; + setup(function() { + var ImportTester = function() {}; + ImportTester.IMPORTS = ['BModule', 'CModule']; + BaseModule.create(ImportTester, { + name: 'ImportTester', + _start: function() {} + }); + importTester = new ImportTester(); + }); + + teardown(function() { + importTester.stop(); + }); + + test('Module start will be executed until import loaded', function() { + var p = new MockPromise(); + this.sinon.stub(MockLazyLoader, 'load', function() { + return p; + }); + var stubCustomStart = this.sinon.stub(importTester, '_start'); + importTester.start(); + assert.isFalse(stubCustomStart.called); + p.mFulfillToValue(); + assert.isTrue(stubCustomStart.called); + }); + }); + + suite('Module life cycle control', function() { + var lifeCycleTester; + setup(function() { + var LifeCycleTester = function() {}; + BaseModule.create(LifeCycleTester, { + name: 'LifeCycleTester', + _start: function() {}, + _stop: function() {} + }); + + lifeCycleTester = new LifeCycleTester(); + }); + + teardown(function() { + lifeCycleTester.stop(); + }); + + test('custom start should be triggered', function() { + var spy = this.sinon.stub(lifeCycleTester, '_start'); + lifeCycleTester.start(); + assert.isTrue(spy.calledOnce); + }); + + test('custom start will not execute if already started', function() { + lifeCycleTester.start(); + var spy = this.sinon.stub(lifeCycleTester, '_start'); + lifeCycleTester.start(); + assert.isFalse(spy.called); + }); + + test('custom stop should be triggered', function() { + var spy = this.sinon.stub(lifeCycleTester, '_stop'); + lifeCycleTester.start(); + lifeCycleTester.stop(); + assert.isTrue(spy.calledOnce); + }); + + test('custom stop will not execute if already stopped', function() { + lifeCycleTester.start(); + lifeCycleTester.stop(); + var spy = this.sinon.stub(lifeCycleTester, '_stop'); + lifeCycleTester.stop(); + assert.isFalse(spy.called); + }); + }); +}); diff --git a/tv_apps/smart-system/test/unit/deferred.js b/tv_apps/smart-system/test/unit/deferred.js new file mode 100644 index 000000000000..dba8e2301b82 --- /dev/null +++ b/tv_apps/smart-system/test/unit/deferred.js @@ -0,0 +1,12 @@ +'use strict'; + +(function(exports) { + var Deferred = function() { + this.promise = new Promise(function(resolve, reject) { + this.resolve = resolve; + this.reject = reject; + }.bind(this)); + return this; + }; + exports.Deferred = Deferred; +}(window));