diff --git a/addons/mail/static/tests/helpers/patch_ui_size.js b/addons/mail/static/tests/helpers/patch_ui_size.js new file mode 100644 index 00000000000..ecb55c0952a --- /dev/null +++ b/addons/mail/static/tests/helpers/patch_ui_size.js @@ -0,0 +1,106 @@ +/** @odoo-module **/ + +import { browser } from '@web/core/browser/browser'; +import { getMediaQueryLists, MEDIAS_BREAKPOINTS, SIZES } from '@web/core/ui/ui_service'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; + +import config from 'web.config'; + +/** + * Return the width corresponding to the given size. If an upper and lower bound + * are defined, returns the lower bound: this is an arbitrary choice that should + * not impact anything. A test should pass the `width` parameter instead of `size` + * if it needs a specific width to be set. + * + * @param {number} size + * @returns {number} The width corresponding to the given size. + */ +function getWidthFromSize(size) { + const { minWidth, maxWidth } = MEDIAS_BREAKPOINTS[size]; + return minWidth ? minWidth : maxWidth; +} + +/** + * Return the size corresponding to the given width. + * + * @param {number} width + * @returns {number} The size corresponding to the given width. + */ +function getSizeFromWidth(width) { + return MEDIAS_BREAKPOINTS.findIndex(({ minWidth, maxWidth }) => { + if (!maxWidth) { + return width >= minWidth; + } + if (!minWidth) { + return width <= maxWidth; + } + return width >= minWidth && width <= maxWidth; + }); +} + +/** + * Patch legacy objects referring to the ui size. This function must be removed + * when switching to the new environment in the discuss app. This will impact + * env.browser.innerWidth, env.device.isMobile and config.device.{size_class/isMobile}. + * + * @param {number} size + * @param {number} width + */ +function legacyPatchUiSize(height, size, width) { + const legacyEnv = owl.Component.env; + patchWithCleanup(legacyEnv, { + browser: { + ...legacyEnv.browser, + innerWidth: width, + innerHeight: height || browser.innerHeight, + }, + device: { + ...legacyEnv.device, + isMobile: size <= SIZES.SM, + } + }); + patchWithCleanup(config, { + device: { + ...config.device, + size_class: size, + isMobile: size <= SIZES.SM, + }, + }); +} + +/** + * Adjust ui size either from given size (mapped to window breakpoints) or + * width. This will impact not only config.device.{size_class/isMobile} but + * uiService.{isSmall/size}, (wowl/legacy) browser.innerWidth, (wowl) + * env.isSmall and (legacy) env.device.isMobile. When a size is given, the browser + * width is set according to the breakpoints that are used by the webClient. + * + * @param {Object} params parameters to configure the ui size. + * @param {number|undefined} [params.size] + * @param {number|undefined} [params.width] + * @param {number|undefined} [params.height] + */ +function patchUiSize({ height, size, width }) { + if (!size && !width || size && width) { + throw new Error('Either size or width must be given to the patchUiSize function'); + } + size = size === undefined ? getSizeFromWidth(width) : size; + width = width || getWidthFromSize(size); + const MEDIAS = getMediaQueryLists(); + + patchWithCleanup(browser, { + innerWidth: width, + innerHeight: height || browser.innerHeight, + }); + patchWithCleanup(window.MediaQueryList.prototype, { + get matches() { + return this.media === MEDIAS[size].media; + }, + }); + legacyPatchUiSize(height, size, width); +} + +export { + patchUiSize, + SIZES +}; diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index 39e7dc8e873..90324c10e11 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -653,7 +653,7 @@ async function start(param0 = {}) { const { autoOpenDiscuss = false, discuss = {}, - env: providedEnv, + env: providedEnv = {}, hasWebClient = false, hasTimeControl = false, hasView = false, @@ -684,7 +684,18 @@ async function start(param0 = {}) { const { debug = false } = param0; initCallbacks.forEach(callback => callback(param0)); const testSetupDoneDeferred = makeDeferred(); - let env = Object.assign(providedEnv || {}); + let env = { + ...providedEnv, + browser: { + ...providedEnv.browser, + innerWidth: browser.innerWidth, + innerHeight: browser.innerHeight, + }, + device: { + ...providedEnv.device, + isMobile: owl.Component.env.device.isMobile, + }, + }; env.session = Object.assign( { is_bound: Promise.resolve(), diff --git a/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js b/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js index 7d4d0b38017..88f6ce1279e 100644 --- a/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js +++ b/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js @@ -1,13 +1,11 @@ /** @odoo-module **/ +import { patchUiSize } from '@mail/../tests/helpers/patch_ui_size'; import { start, startServer, } from '@mail/../tests/helpers/test_utils'; -import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; - QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { QUnit.module('discuss_mobile_mailbox_selection', {}, function () { @@ -16,17 +14,9 @@ QUnit.module('discuss_mobile_mailbox_selection_tests.js'); QUnit.test('select another mailbox', async function (assert) { assert.expect(7); - patchWithCleanup(browser, { - innerHeight: 640, - innerWidth: 360, - }); + patchUiSize({ height: 360, width: 640 }); const { click, messaging } = await start({ autoOpenDiscuss: true, - env: { - device: { - isMobile: true, - }, - }, }); assert.containsOnce( document.body, @@ -77,10 +67,7 @@ QUnit.test('auto-select "Inbox" when discuss had channel as active thread', asyn const pyEnv = await startServer(); const mailChannelId1 = pyEnv['mail.channel'].create(); - patchWithCleanup(browser, { - innerHeight: 640, - innerWidth: 360, - }); + patchUiSize({ height: 360, width: 640 }); const { click, messaging } = await start({ autoOpenDiscuss: true, discuss: { @@ -88,11 +75,6 @@ QUnit.test('auto-select "Inbox" when discuss had channel as active thread', asyn active_id: mailChannelId1, }, }, - env: { - device: { - isMobile: true, - }, - }, }); assert.hasClass( document.querySelector('.o_MobileMessagingNavbar_tab[data-tab-id="channel"]'), diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js index 0b31c25cb5c..a31a40a3ae4 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { makeDeferred } from '@mail/utils/deferred'; +import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size'; import { afterNextRender, nextAnimationFrame, @@ -8,9 +9,6 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; - import { file, dom } from 'web.test_utils'; const { createFile, inputFiles } = file; const { triggerEvent } = dom; @@ -236,9 +234,7 @@ QUnit.test('open chat from "new message" chat window should open chat in place o }, ]); const imSearchDef = makeDeferred(); - patchWithCleanup(browser, { - innerWidth: 1920, - }); + patchUiSize({ width: 1920 }); const { click, createMessagingMenuComponent } = await start({ async mockRPC(route, args) { const res = await this._super(...arguments); @@ -629,13 +625,8 @@ QUnit.test('Mobile: opening a chat window should not update channel state on the }], ], }); - const { click, createMessagingMenuComponent } = await start({ - env: { - device: { - isMobile: true, - }, - }, - }); + patchUiSize({ size: SIZES.SM }); + const { click, createMessagingMenuComponent } = await start(); await createMessagingMenuComponent(); await click(`.o_MessagingMenu_toggler`); await click(`.o_NotificationList_preview`); @@ -664,13 +655,8 @@ QUnit.test('Mobile: closing a chat window should not update channel state on the }], ], }); - const { click, createMessagingMenuComponent } = await start({ - env: { - device: { - isMobile: true, - }, - }, - }); + patchUiSize({ size: SIZES.SM }); + const { click, createMessagingMenuComponent } = await start(); await createMessagingMenuComponent(); await click(`.o_MessagingMenu_toggler`); await click(`.o_NotificationList_preview`); @@ -711,13 +697,8 @@ QUnit.test("Mobile: chat window shouldn't open automatically after receiving a n uuid: 'channel-10-uuid', }, ]; - const { messaging } = await start({ - env: { - device: { - isMobile: true, - }, - }, - }); + patchUiSize({ size: SIZES.SM }); + const { messaging } = await start(); // simulate receiving a message messaging.rpc({ @@ -858,9 +839,7 @@ QUnit.test('focus next visible chat window when closing current chat window with ], }, ]); - patchWithCleanup(browser, { - innerWidth: 1920, - }); + patchUiSize({ width: 1920 }); await start(); assert.containsN( document.body, @@ -1041,9 +1020,7 @@ QUnit.test('open 2 different chat windows: enough screen width [REQUIRE FOCUS]', const pyEnv = await startServer(); const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]); - patchWithCleanup(browser, { - innerWidth: 1920, // enough to fit at least 2 chat windows - }); + patchUiSize({ width: 1920 }); // enough to fit at least 2 chat windows const { click, createMessagingMenuComponent, messaging } = await start(); await createMessagingMenuComponent(); await click(`.o_MessagingMenu_toggler`); @@ -1267,9 +1244,7 @@ QUnit.test('open 2 folded chat windows: check shift operations are available', a channel_type: "chat", }; pyEnv['mail.channel'].create([channel, chat]); - patchWithCleanup(browser, { - innerWidth: 900, - }); + patchUiSize({ width: 900 }); const { click } = await start(); assert.containsN( @@ -1377,9 +1352,7 @@ QUnit.test('open 3 different chat windows: not enough screen width', async funct { name: 'mailChannel2' }, { name: 'mailChannel3' }, ]); - patchWithCleanup(browser, { - innerWidth: 900, // enough to fit 2 chat windows but not 3 - }); + patchUiSize({ width: 900 }); // enough to fit 2 chat windows but not 3 const { click, createMessagingMenuComponent, messaging } = await start(); await createMessagingMenuComponent(); @@ -1633,9 +1606,7 @@ QUnit.test('chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]', as ], }, ]); - patchWithCleanup(browser, { - innerWidth: 1920, - }); + patchUiSize({ width: 1920 }); await start(); assert.containsN( document.body, @@ -1830,13 +1801,8 @@ QUnit.test('chat window: post message on non-mailing channel with "CTRL-Enter" k }], ], }); - const { click, createMessagingMenuComponent, insertText } = await start({ - env: { - device: { - isMobile: true, // here isMobile is used for the small screen size, not actually for the mobile devices - }, - }, - }); + patchUiSize({ size: SIZES.SM }); + const { click, createMessagingMenuComponent, insertText } = await start(); await createMessagingMenuComponent(); await click(`.o_MessagingMenu_toggler`); @@ -2046,9 +2012,7 @@ QUnit.test('chat window does not fetch messages if hidden', async function (asse name: "Channel #12", }, ]); - patchWithCleanup(browser, { - innerWidth: 900, - }); + patchUiSize({ width: 900 }); const { click, messaging } = await start({ mockRPC(route, args) { if (route === '/mail/channel/messages') { @@ -2449,9 +2413,7 @@ QUnit.test('should not have chat window hidden menu in mobile (transition from 2 const pyEnv = await startServer(); const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel1' }]); - patchWithCleanup(browser, { - innerWidth: 600, // enough to fit 1 chat window + hidden menu - }); + patchUiSize({ width: 600 }); // enough to fit 1 chat window + hidden menu const { click, createMessagingMenuComponent, messaging } = await start(); await createMessagingMenuComponent(); // open, from systray menu, chat windows of channels with id 1, 2 @@ -2465,7 +2427,7 @@ QUnit.test('should not have chat window hidden menu in mobile (transition from 2 }).localId }"] `); - await click('.o_MessagingMenu_toggler'); + await click('.o_ChatWindowHeader_commandBack'); await click(` .o_MessagingMenu_dropdownMenu .o_NotificationList_preview[data-thread-local-id="${ diff --git a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js index f65df583a1d..2c4e8e6fc3e 100644 --- a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { makeDeferred } from '@mail/utils/deferred'; +import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size'; import { afterNextRender, nextAnimationFrame, @@ -8,7 +9,6 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import config from 'web.config'; import fieldRegistry from 'web.field_registry'; import FormView from 'web.FormView'; import { dom, nextTick } from 'web.test_utils'; @@ -775,6 +775,7 @@ QUnit.test('Form view not scrolled when switching record', async function (asser }); pyEnv['mail.message'].create(messages); + patchUiSize({ size: SIZES.LG }); const { click } = await this.createView({ hasView: true, // View params @@ -795,12 +796,6 @@ QUnit.test('Form view not scrolled when switching record', async function (asser currentId: resPartnerId1, ids: [resPartnerId1, resPartnerId2], }, - config: { - device: { size_class: config.device.SIZES.LG }, - }, - env: { - device: { size_class: config.device.SIZES.LG }, - }, }); const controllerContentEl = document.querySelector('.o_content'); diff --git a/addons/web/static/src/core/ui/ui_service.js b/addons/web/static/src/core/ui/ui_service.js index ed6f0000344..a50964e7be6 100644 --- a/addons/web/static/src/core/ui/ui_service.js +++ b/addons/web/static/src/core/ui/ui_service.js @@ -38,6 +38,35 @@ export function useActiveElement(refName) { ); } +// window size handling +export const MEDIAS_BREAKPOINTS = [ + { maxWidth: 474 }, + { minWidth: 475, maxWidth: 575 }, + { minWidth: 576, maxWidth: 767 }, + { minWidth: 769, maxWidth: 991 }, + { minWidth: 992, maxWidth: 1199 }, + { minWidth: 1200, maxWidth: 1533 }, + { minWidth: 1534 }, +]; + +/** + * Create the MediaQueryList used both by the uiService and config from + * `MEDIA_BREAKPOINTS`. + * + * @returns {MediaQueryList[]} + */ + export function getMediaQueryLists() { + return MEDIAS_BREAKPOINTS.map(({ minWidth, maxWidth }) => { + if (!maxWidth) { + return window.matchMedia(`(min-width: ${minWidth}px)`); + } + if (!minWidth) { + return window.matchMedia(`(max-width: ${maxWidth}px)`); + } + return window.matchMedia(`(min-width: ${minWidth}px) and (max-width: ${maxWidth}px)`); + }); +} + export const uiService = { start(env) { let ui = {}; @@ -109,15 +138,7 @@ export const uiService = { }); // window size handling - const MEDIAS = [ - window.matchMedia("(max-width: 474px)"), - window.matchMedia("(min-width: 475px) and (max-width: 575px)"), - window.matchMedia("(min-width: 576px) and (max-width: 767px)"), - window.matchMedia("(min-width: 768px) and (max-width: 991px)"), - window.matchMedia("(min-width: 992px) and (max-width: 1199px)"), - window.matchMedia("(min-width: 1200px) and (max-width: 1533px)"), - window.matchMedia("(min-width: 1534px)"), - ]; + const MEDIAS = getMediaQueryLists(); function getSize() { return MEDIAS.findIndex((media) => media.matches); } diff --git a/addons/web/static/src/legacy/js/services/config.js b/addons/web/static/src/legacy/js/services/config.js index 45f5b00aabc..9d49280db6b 100644 --- a/addons/web/static/src/legacy/js/services/config.js +++ b/addons/web/static/src/legacy/js/services/config.js @@ -3,6 +3,7 @@ odoo.define('web.config', function (require) { const Bus = require('web.Bus'); const { hasTouch, isAndroid, isIOS, isMobileOS } = require('@web/core/browser/feature_detection'); +const { getMediaQueryLists } = require('@web/core/ui/ui_service'); const bus = new Bus(); @@ -87,16 +88,7 @@ var config = { }, }; - -var medias = [ - window.matchMedia('(max-width: 474px)'), - window.matchMedia('(min-width: 475px) and (max-width: 575px)'), - window.matchMedia('(min-width: 576px) and (max-width: 767px)'), - window.matchMedia('(min-width: 768px) and (max-width: 991px)'), - window.matchMedia('(min-width: 992px) and (max-width: 1199px)'), - window.matchMedia('(min-width: 1200px) and (max-width: 1533px)'), - window.matchMedia('(min-width: 1534px)'), -]; +const medias = getMediaQueryLists(); /** * Return the current size class