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 @@