[IMP] mail, web: allow ui size patching during tests

Until now, when we add to patch the ui size, we would have patched one of the following:
	- config.device.{isMobile,size_class}
	- (wowl) env.isSmall / env.services.ui.{isSmall/size}
	- (legacy) env.device.isMobile
	- (legacy/wowl) browser.innerWidth

The issue is that patching one does not affect the others resulting in an incoherent state.
In order to ease ui size patching, the `patchUiSize` method has been introduced. This method
take either a size or a width as parameter (eg. SIZES.SM, 1920) and affect all the properties above.

task-2582313

closes odoo/odoo#91540

Related: odoo/enterprise#27428
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
tsm-odoo
2022-05-19 16:40:12 +02:00
parent fe2f4fee18
commit 3c10fcadee
7 changed files with 174 additions and 105 deletions
@@ -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
};
+13 -2
View File
@@ -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(),
@@ -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"]'),
@@ -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="${
@@ -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');
+30 -9
View File
@@ -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);
}
@@ -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