diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js
index 647d0fe2563..50608b451cb 100644
--- a/addons/web_tour/static/src/js/tour_manager.js
+++ b/addons/web_tour/static/src/js/tour_manager.js
@@ -51,6 +51,9 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
* the url to load when manually running the tour
* @param {boolean} [options.rainbowMan=true]
* whether or not the rainbowman must be shown at the end of the tour
+ * @param {boolean} [options.sequence=1000]
+ * priority sequence of the tour (lowest is first, tours with the same
+ * sequence will be executed in a non deterministic order).
* @param {Promise} [options.wait_for]
* indicates when the tour can be started
* @param {Object[]} steps - steps' descriptions, each step being an object
@@ -71,6 +74,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
steps: steps,
url: options.url,
rainbowMan: options.rainbowMan === undefined ? true : !!options.rainbowMan,
+ sequence: options.sequence || 1000,
test: options.test,
wait_for: options.wait_for || Promise.resolve(),
};
@@ -113,7 +117,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
_register: function (do_update, tour, name) {
if (tour.ready) return Promise.resolve();
- var tour_is_consumed = _.contains(this.consumed_tours, name);
+ const tour_is_consumed = this._isTourConsumed(name);
return tour.wait_for.then((function () {
tour.current_step = parseInt(local_storage.getItem(get_step_key(name))) || 0;
@@ -198,7 +202,10 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
self._check_for_tooltip(self.active_tooltips[tour_name], tour_name);
});
} else {
- for (var tourName in this.active_tooltips) {
+ const sortedTooltips = Object.keys(this.active_tooltips).sort(
+ (a, b) => this.tours[a].sequence - this.tours[b].sequence
+ );
+ for (const tourName of sortedTooltips) {
var tip = this.active_tooltips[tourName];
var activated = this._check_for_tooltip(tip, tourName);
if (activated) {
@@ -342,6 +349,14 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
}
this.active_tooltips[tour_name] = tour.steps[tour.current_step];
},
+ /**
+ * @private
+ * @param {string} tourName
+ * @returns {boolean}
+ */
+ _isTourConsumed(tourName) {
+ return this.consumed_tours.includes(tourName);
+ },
_consume_tour: function (tour_name, error) {
delete this.active_tooltips[tour_name];
//display rainbow at the end of any tour
diff --git a/addons/web_tour/static/tests/tour_manager_tests.js b/addons/web_tour/static/tests/tour_manager_tests.js
new file mode 100644
index 00000000000..9d285732f06
--- /dev/null
+++ b/addons/web_tour/static/tests/tour_manager_tests.js
@@ -0,0 +1,71 @@
+odoo.define('web_tour.tour_manager_tests', function (require) {
+ "use strict";
+
+ const TourManager = require('web_tour.TourManager');
+ const testUtils = require('web.test_utils');
+
+ /**
+ * Create a widget and a TourManager instance with a list of given Tour objects.
+ * @see TourManager.register() for more details on the Tours registry system.
+ * @param {Object} params
+ * @param {string[]} [params.consumed_tours]
+ * @param {boolean} [params.debug]
+ * @param {string} params.template inner HTML content of the widget
+ * @param {Object[]} params.tours { {string} name, {Object} option, {Object[]} steps }
+ */
+ async function createTourManager({ consumed_tours, debug, template, tours }) {
+ const parent = testUtils.createParent({ debug });
+ const tourManager = new TourManager(parent, consumed_tours);
+ tourManager.running_step_delay = 0;
+ for (const { name, options, steps } of tours) {
+ tourManager.register(name, options, steps);
+ }
+ const _destroy = tourManager.destroy;
+ tourManager.destroy = function () {
+ tourManager.destroy = _destroy;
+ parent.destroy();
+ };
+ await parent.prependTo(testUtils.prepareTarget(debug));
+ parent.el.innerHTML = template;
+ testUtils.mock.patch(TourManager, {
+ // Since the `tour_disable.js` script automatically sets tours as consumed
+ // as soon as they are registered, we override the "is consumed" to
+ // assert that the tour is in the `consumed_tours` param key.
+ _isTourConsumed: name => (consumed_tours || []).includes(name),
+ });
+ await tourManager._register_all(true);
+ // Wait for possible tooltips to be loaded and appended.
+ await testUtils.nextTick();
+ return tourManager;
+ }
+
+ QUnit.module("Tours", {
+ afterEach() {
+ testUtils.mock.unpatch(TourManager);
+ },
+ }, function () {
+
+ QUnit.module("Tour manager");
+
+ QUnit.test("Tours sequence", async function (assert) {
+ assert.expect(2);
+
+ const tourManager = await createTourManager({
+ template: `
+ `,
+ tours: [
+ { name: "Tour 1", options: { sequence: 10 }, steps: [{ trigger: '.anchor' }] },
+ { name: "Tour 2", options: {}, steps: [{ trigger: '.anchor' }] },
+ { name: "Tour 3", options: { sequence: 5 }, steps: [{ trigger: '.anchor', content: "Oui" }] },
+ ],
+ });
+
+ assert.containsOnce(document.body, '.o_tooltip');
+ const tooltip = document.querySelector('.o_tooltip_content');
+ assert.strictEqual(tooltip.innerHTML.trim(), "Oui",
+ "content should be that of the third tour");
+
+ tourManager.destroy();
+ });
+ });
+});
diff --git a/addons/web_tour/views/tour_templates.xml b/addons/web_tour/views/tour_templates.xml
index 09376487f73..113ac3ab06c 100644
--- a/addons/web_tour/views/tour_templates.xml
+++ b/addons/web_tour/views/tour_templates.xml
@@ -25,5 +25,11 @@
+
+
+
+
+
+