diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index fdb029da6c3..f4def9f69c8 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -185,8 +185,24 @@ var AbstractView = Class.extend({
*/
_loadLibs: function () {
var defs = [];
- _.each(this.config.js_libs, function (url) {
- defs.push(ajax.loadJS(url));
+ var jsDefs;
+ _.each(this.config.js_libs, function (urls) {
+ if (typeof(urls) === 'string') {
+ // js_libs is an array of urls: those urls can be loaded in
+ // parallel
+ defs.push(ajax.loadJS(urls));
+ } else {
+ // js_libs is an array of arrays of urls: those arrays of urls
+ // must be loaded sequentially, but the urls inside each
+ // sub-array can be loaded in parallel
+ defs.push($.when.apply($, jsDefs).then(function () {
+ jsDefs = [];
+ _.each(urls, function (url) {
+ jsDefs.push(ajax.loadJS(url));
+ });
+ return $.when.apply($, jsDefs);
+ }));
+ }
});
_.each(this.config.css_libs, function (url) {
defs.push(ajax.loadCSS(url));
diff --git a/addons/web/static/tests/views/abstract_view_tests.js b/addons/web/static/tests/views/abstract_view_tests.js
new file mode 100644
index 00000000000..1c85b390cc6
--- /dev/null
+++ b/addons/web/static/tests/views/abstract_view_tests.js
@@ -0,0 +1,106 @@
+odoo.define('web.abstract_view_tests', function (require) {
+"use strict";
+
+var AbstractView = require('web.AbstractView');
+var ajax = require('web.ajax');
+var testUtils = require('web.test_utils');
+
+var createAsyncView = testUtils.createAsyncView;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ fake_model: {
+ fields: {},
+ record: [],
+ },
+ };
+ },
+}, function () {
+
+ QUnit.module('AbstractView');
+
+ QUnit.test('lazy loading of js libs (in parallel)', function (assert) {
+ var done = assert.async();
+ assert.expect(6);
+
+ var def = $.Deferred();
+ var loadJS = ajax.loadJS;
+ ajax.loadJS = function (url) {
+ assert.step(url);
+ return def.then(function () {
+ assert.step(url + ' loaded');
+ });
+ };
+ var View = AbstractView.extend({
+ config: _.extend({}, AbstractView.prototype.config, {
+ js_libs: ['a', 'b'],
+ }),
+ });
+ createAsyncView({
+ View: View,
+ arch: '',
+ data: this.data,
+ model: 'fake_model',
+ }).then(function (view) {
+ assert.verifySteps(['a', 'b', 'a loaded', 'b loaded'],
+ "should wait for both libs to be loaded");
+ ajax.loadJS = loadJS;
+ view.destroy();
+ done();
+ });
+
+ assert.verifySteps(['a', 'b'],
+ "both libs should be loaded in parallel");
+ def.resolve();
+ });
+
+ QUnit.test('lazy loading of js libs (sequentially)', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ var defs = {
+ a: $.Deferred(),
+ b: $.Deferred(),
+ c: $.Deferred(),
+ };
+ var loadJS = ajax.loadJS;
+ ajax.loadJS = function (url) {
+ assert.step(url);
+ return defs[url].then(function () {
+ assert.step(url + ' loaded');
+ });
+ };
+ var View = AbstractView.extend({
+ config: _.extend({}, AbstractView.prototype.config, {
+ js_libs: [
+ ['a', 'b'],
+ ['c'],
+ ],
+ }),
+ });
+ createAsyncView({
+ View: View,
+ arch: '',
+ data: this.data,
+ model: 'fake_model',
+ }).then(function (view) {
+ assert.verifySteps(['a', 'b', 'a loaded', 'b loaded', 'c', 'c loaded'],
+ "should for all libs to be loaded");
+ ajax.loadJS = loadJS;
+ view.destroy();
+ done();
+ });
+
+ assert.verifySteps(['a', 'b'],
+ "libs 'a' and 'b' should be loaded in parallel");
+ defs.a.resolve();
+ assert.verifySteps(['a', 'b', 'a loaded'],
+ "should wait for 'a' and 'b' to be loaded before loading 'c'");
+ defs.b.resolve();
+ assert.verifySteps(['a', 'b', 'a loaded', 'b loaded', 'c'],
+ "should load 'c' when 'a' and 'b' are loaded");
+ defs.c.resolve();
+ });
+});
+});
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 8eeb2c7eba5..27831759b4b 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -434,6 +434,7 @@
+