[IMP] web, *: use MutationObserver to neutralize image src in tests
*: mail, mrp Previously, to prevent images and iframes from making GET requests in tests, we would listen for the DOMNodeInserted event. This event is deprecated and should not be used. Additionally, we weren't listening for attiribute changes for performance reasons, which would result in some GET requests still being made when an src attribute was changed. This commit changes the mechanism that is used to do this from mutation events to a MutationObserver which is not deprecated, and now also listens for attribute modifications as MutationObservers can be configured to only listen for changes to certain attributes, greatly alleviating the performance impact of listening for those. This mechanism has also been enabled globally by doing it once in the test setup code, instead of requiring one to call a specific helper, as we never want to make real requests during tests. Some tests have been adapted to this new behaviour. closes odoo/odoo#85216 Related: odoo/enterprise#24665 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
+1
-2
@@ -101,9 +101,8 @@ QUnit.test('channel - avatar: should update avatar url from bus', async function
|
||||
});
|
||||
const newCacheKey = result[0]['avatarCacheKey'];
|
||||
|
||||
// FIXME: current test framework does not replace `src` with `data-src` during the re-rendering.
|
||||
assert.strictEqual(
|
||||
channelItemAvatar.getAttribute('src'),
|
||||
channelItemAvatar.dataset.src,
|
||||
`/web/image/mail.channel/20/avatar_128?unique=${newCacheKey}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -580,7 +580,7 @@ QUnit.test('Activity view: many2one_avatar_user widget in activity view', async
|
||||
|
||||
await legacyExtraNextTick();
|
||||
assert.containsN(target, '.o_m2o_avatar', 2);
|
||||
assert.containsOnce(target, 'tr[data-res-id=13] .o_m2o_avatar > img[src="/web/image/res.users/1/avatar_128"]',
|
||||
assert.containsOnce(target, 'tr[data-res-id=13] .o_m2o_avatar > img[data-src="/web/image/res.users/1/avatar_128"]',
|
||||
"should have m2o avatar image");
|
||||
assert.containsNone(target, '.o_m2o_avatar > span',
|
||||
"should not have text on many2one_avatar_user if onlyImage node option is passed");
|
||||
|
||||
@@ -102,23 +102,16 @@ QUnit.module('document_viewer_tests.js', {
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer Youtube', async function (assert) {
|
||||
assert.expect(3);
|
||||
assert.expect(2);
|
||||
|
||||
var youtubeURL = 'https://www.youtube.com/embed/FYqW0Gdwbzk';
|
||||
var viewer = await createViewer({
|
||||
attachmentID: 2,
|
||||
attachments: this.attachments,
|
||||
mockRPC: function (route) {
|
||||
if (route === youtubeURL) {
|
||||
assert.ok(true, "should have called youtube URL");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('urlYoutube')").length, 1,
|
||||
"the viewer should be on the right attachment");
|
||||
assert.containsOnce(viewer, '.o_viewer_text[data-src="' + youtubeURL + '"]',
|
||||
assert.containsOnce(viewer, '.o_viewer_text[data-src="https://www.youtube.com/embed/FYqW0Gdwbzk"]',
|
||||
"there should be a video player");
|
||||
|
||||
viewer.destroy();
|
||||
|
||||
@@ -92,7 +92,7 @@ QUnit.module('mrp', {
|
||||
await testUtils.form.clickEdit(form);
|
||||
assert.isNotVisible(form.$('iframe.o_embed_iframe'), "there should be an invisible iframe in edit mode");
|
||||
await testUtils.fields.editAndTrigger(form.$('.o_field_char'), 'http://example.com', ['input', 'change', 'focusout']);
|
||||
assert.strictEqual(form.$('iframe.o_embed_iframe').attr('src'), 'http://example.com',
|
||||
assert.strictEqual(form.$('iframe.o_embed_iframe').attr('data-src'), 'http://example.com',
|
||||
"src should updated on the iframe");
|
||||
assert.isVisible(form.$('iframe.o_embed_iframe'), "there should be a visible iframe in edit mode");
|
||||
await testUtils.form.clickSave(form);
|
||||
|
||||
@@ -2958,10 +2958,6 @@ QUnit.module('basic_fields', {
|
||||
if (route === '/web/dataset/call_kw/partner/read') {
|
||||
assert.deepEqual(args.args[1], ['document', '__last_update', 'display_name'], "The fields document, display_name and __last_update should be present when reading an image");
|
||||
}
|
||||
if (route === `data:image/png;base64,${MY_IMAGE}`) {
|
||||
assert.ok(true, "should called the correct route");
|
||||
return 'wow';
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
@@ -2970,6 +2966,8 @@ QUnit.module('basic_fields', {
|
||||
"the widget should have the correct class");
|
||||
assert.containsOnce(form, 'div[name="document"] > img',
|
||||
"the widget should contain an image");
|
||||
assert.strictEqual(form.$('div[name="document"] > img')[0].dataset.src,
|
||||
`data:image/png;base64,${MY_IMAGE}`, "the image should have the correct src");
|
||||
assert.hasClass(form.$('div[name="document"] > img'),'img-fluid',
|
||||
"the image should have the correct class");
|
||||
assert.hasAttrValue(form.$('div[name="document"] > img'), 'width', "90",
|
||||
@@ -2980,7 +2978,7 @@ QUnit.module('basic_fields', {
|
||||
});
|
||||
|
||||
QUnit.test('image fields are correctly replaced when given an incorrect value', async function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(6);
|
||||
|
||||
this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
|
||||
this.data.partner.records[0].document = 'incorrect_base64_value';
|
||||
@@ -3006,19 +3004,14 @@ QUnit.module('basic_fields', {
|
||||
<field name="document" widget="image" options="{'size': [90, 90]}"/>
|
||||
</form>`,
|
||||
res_id: 1,
|
||||
async mockRPC(route, args) {
|
||||
const _super = this._super;
|
||||
if (route === '/web/static/img/placeholder.png') {
|
||||
assert.step('call placeholder route');
|
||||
}
|
||||
return _super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(form.$('div[name="document"]'),'o_field_image',
|
||||
"the widget should have the correct class");
|
||||
assert.containsOnce(form, 'div[name="document"] > img',
|
||||
"the widget should contain an image");
|
||||
assert.strictEqual(form.$('div[name="document"] > img')[0].dataset.src,
|
||||
`/web/static/img/placeholder.png`, "the image should have the correct src");
|
||||
assert.hasClass(form.$('div[name="document"] > img'), 'img-fluid',
|
||||
"the image should have the correct class");
|
||||
assert.hasAttrValue(form.$('div[name="document"] > img'), 'width', "90",
|
||||
@@ -3026,7 +3019,6 @@ QUnit.module('basic_fields', {
|
||||
assert.strictEqual(form.$('div[name="document"] > img').css('max-width'), "90px",
|
||||
"the image should correctly set its attributes");
|
||||
|
||||
assert.verifySteps(['call placeholder route']);
|
||||
|
||||
form.destroy();
|
||||
testUtils.mock.unpatch(basicFields.FieldBinaryImage);
|
||||
@@ -3061,7 +3053,7 @@ QUnit.module('basic_fields', {
|
||||
});
|
||||
|
||||
QUnit.test('image fields in subviews are loaded correctly', async function (assert) {
|
||||
assert.expect(6);
|
||||
assert.expect(4);
|
||||
|
||||
this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
|
||||
this.data.partner.records[0].document = MY_IMAGE;
|
||||
@@ -3094,19 +3086,11 @@ QUnit.module('basic_fields', {
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
async mockRPC(route) {
|
||||
if (route === `data:image/png;base64,${MY_IMAGE}`) {
|
||||
assert.step("The view's image should have been fetched");
|
||||
return 'wow';
|
||||
}
|
||||
if (route === `data:image/gif;base64,${PRODUCT_IMAGE}`) {
|
||||
assert.step("The dialog's image should have been fetched");
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
assert.verifySteps(["The view's image should have been fetched"]);
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="data:image/png;base64,${MY_IMAGE}"]`),
|
||||
"The view's image is in the DOM"
|
||||
);
|
||||
|
||||
assert.containsOnce(form, '.o_kanban_record.oe_kanban_global_click',
|
||||
'There should be one record in the many2many');
|
||||
@@ -3115,7 +3099,10 @@ QUnit.module('basic_fields', {
|
||||
await testUtils.dom.click(form.$('.oe_kanban_global_click'));
|
||||
assert.strictEqual($('.modal').length, 1,
|
||||
'The modal should have opened');
|
||||
assert.verifySteps(["The dialog's image should have been fetched"]);
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`),
|
||||
"The dialog's image is in the DOM"
|
||||
);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -3139,17 +3126,14 @@ QUnit.module('basic_fields', {
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
async mockRPC(route) {
|
||||
if (route === `data:image/gif;base64,${PRODUCT_IMAGE}`) {
|
||||
assert.ok(true, "The list's image should have been fetched");
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
'There should be one record in the many2many');
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="data:image/gif;base64,${PRODUCT_IMAGE}"]`),
|
||||
"The list's image is in the DOM"
|
||||
);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -3203,18 +3187,14 @@ QUnit.module('basic_fields', {
|
||||
'<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/> ' +
|
||||
'</form>',
|
||||
res_id: 6,
|
||||
async mockRPC(route, args) {
|
||||
if (route === FR_FLAG_URL) {
|
||||
assert.ok(true, "the correct route should have been called.");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(form.$('div[name="foo"]'), 'o_field_image',
|
||||
"the widget should have the correct class");
|
||||
assert.containsOnce(form, 'div[name="foo"] > img',
|
||||
"the widget should contain an image");
|
||||
assert.strictEqual(form.$('div[name="foo"] > img')[0].dataset.src,
|
||||
FR_FLAG_URL, "the image should have the correct src");
|
||||
assert.hasClass(form.$('div[name="foo"] > img'), 'img-fluid',
|
||||
"the image should have the correct class");
|
||||
assert.hasAttrValue(form.$('div[name="foo"] > img'), 'width', "90",
|
||||
@@ -3225,7 +3205,7 @@ QUnit.module('basic_fields', {
|
||||
});
|
||||
|
||||
QUnit.test('image_url widget in subviews are loaded correctly', async function (assert) {
|
||||
assert.expect(6);
|
||||
assert.expect(4);
|
||||
|
||||
this.data.partner_type.fields.image = {name: 'image', type: 'char'};
|
||||
this.data.partner_type.records[0].image = EN_FLAG_URL;
|
||||
@@ -3256,19 +3236,11 @@ QUnit.module('basic_fields', {
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 6,
|
||||
async mockRPC(route) {
|
||||
if (route === FR_FLAG_URL) {
|
||||
assert.step("The view's image should have been fetched");
|
||||
return 'wow';
|
||||
}
|
||||
if (route === EN_FLAG_URL) {
|
||||
assert.step("The dialog's image should have been fetched");
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
assert.verifySteps(["The view's image should have been fetched"]);
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${FR_FLAG_URL}"]`),
|
||||
"The view's image is in the DOM"
|
||||
);
|
||||
|
||||
assert.containsOnce(form, '.o_kanban_record.oe_kanban_global_click',
|
||||
'There should be one record in the many2many');
|
||||
@@ -3277,7 +3249,10 @@ QUnit.module('basic_fields', {
|
||||
await testUtils.dom.click(form.$('.oe_kanban_global_click'));
|
||||
assert.strictEqual($('.modal').length, 1,
|
||||
'The modal should have opened');
|
||||
assert.verifySteps(["The dialog's image should have been fetched"]);
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
|
||||
"The dialog's image is in the DOM"
|
||||
);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -3301,17 +3276,14 @@ QUnit.module('basic_fields', {
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 6,
|
||||
async mockRPC(route) {
|
||||
if (route === EN_FLAG_URL) {
|
||||
assert.ok(true, "The list's image should have been fetched");
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
'There should be one record in the many2many');
|
||||
assert.ok(
|
||||
document.querySelector(`img[data-src="${EN_FLAG_URL}"]`),
|
||||
"The list's image is in the DOM"
|
||||
);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -287,44 +287,6 @@ function intercept(widget, eventName, fn, propagate) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the src attribute on images and iframes to prevent not found errors,
|
||||
* and optionally triggers an rpc with the src url as route on a widget.
|
||||
* This method is critical and must be fastest (=> no jQuery, no underscore)
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* @param {[function]} rpc
|
||||
*/
|
||||
function removeSrcAttribute(el, rpc) {
|
||||
var nodes;
|
||||
if (el.nodeName === "#comment") {
|
||||
return;
|
||||
}
|
||||
el = el.nodeType === 8 ? el.nextSibling : el;
|
||||
if (el.nodeName === 'IMG' || el.nodeName === 'IFRAME') {
|
||||
nodes = [el];
|
||||
} else {
|
||||
nodes = Array.prototype.slice.call(el.getElementsByTagName('img'))
|
||||
.concat(Array.prototype.slice.call(el.getElementsByTagName('iframe')));
|
||||
}
|
||||
var node;
|
||||
while (node = nodes.pop()) {
|
||||
var src = node.attributes.src && node.attributes.src.value;
|
||||
if (src && src !== 'about:blank') {
|
||||
node.setAttribute('data-src', src);
|
||||
if (node.nodeName === 'IMG') {
|
||||
node.attributes.removeNamedItem('src');
|
||||
} else {
|
||||
node.setAttribute('src', 'about:blank');
|
||||
}
|
||||
if (rpc) {
|
||||
rpc(src, []);
|
||||
}
|
||||
$(node).trigger('load');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mock environment to test Owl Components. This function generates a test
|
||||
* env and sets it on the given Component. It also has several side effects,
|
||||
@@ -396,15 +358,6 @@ async function addMockEnvironmentOwl(Component, params, mockServer) {
|
||||
};
|
||||
}
|
||||
|
||||
// make sure images do not trigger a GET on the server
|
||||
$('body').on('DOMNodeInserted.removeSRC', function (ev) {
|
||||
let rpc;
|
||||
if (params.mockSRC) {
|
||||
rpc = mockServer.performRpc.bind(mockServer);
|
||||
}
|
||||
removeSrcAttribute(ev.target, rpc);
|
||||
});
|
||||
|
||||
// mock global objects for legacy widgets (session, config...)
|
||||
const restoreMockedGlobalObjects = _mockGlobalObjects(params);
|
||||
|
||||
|
||||
@@ -7124,13 +7124,6 @@ QUnit.module('Views', {
|
||||
'<img t-att-src="kanban_image(\'partner\', \'image\', record.id.raw_value)"/>' +
|
||||
'</div></t></templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==') {
|
||||
assert.ok("The view's image should have been fetched.");
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
var images = kanban.el.querySelectorAll('img');
|
||||
var placeholders = [];
|
||||
@@ -7143,6 +7136,8 @@ QUnit.module('Views', {
|
||||
|
||||
assert.strictEqual(placeholders.length, this.data.partner.records.length - 1,
|
||||
"partner with no image should display the placeholder");
|
||||
assert.strictEqual(images[0].dataset.src, "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==",
|
||||
"The first partners non-placeholder image should be set");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
@@ -7161,13 +7156,6 @@ QUnit.module('Views', {
|
||||
'<img t-att-src="kanban_image(\'partner\', \'image\', 1)"/>' +
|
||||
'</div></t></templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==') {
|
||||
assert.ok("The view's image should have been fetched.");
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// the field image is set, but we request the image for a specific id
|
||||
@@ -7176,6 +7164,9 @@ QUnit.module('Views', {
|
||||
var imageOnRecord = kanban.$('img[data-src*="/web/image"][data-src*="&id=1"]');
|
||||
assert.strictEqual(imageOnRecord.length, this.data.partner.records.length - 1,
|
||||
"display image by url when requested for another record");
|
||||
assert.strictEqual(kanban.el.querySelector("img").dataset.src,
|
||||
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA==",
|
||||
"display image as value when requested for the record itself");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
@@ -222,4 +222,33 @@ export async function setupTests() {
|
||||
const templates = await loadFile(templatesUrl);
|
||||
window.__OWL_TEMPLATES__ = processTemplates(templates);
|
||||
await Promise.all([whenReady(), legacyProm]);
|
||||
|
||||
// make sure images do not trigger a GET on the server
|
||||
new MutationObserver((mutations) => {
|
||||
const nodes = mutations.flatMap(({ target }) => {
|
||||
if (target.nodeName === "IMG" || target.nodeName === "IFRAME") {
|
||||
return target;
|
||||
}
|
||||
return [
|
||||
...target.getElementsByTagName("img"),
|
||||
...target.getElementsByTagName("iframe"),
|
||||
];
|
||||
});
|
||||
for (const node of nodes) {
|
||||
const src = node.getAttribute("src");
|
||||
if (src && src !== "about:blank") {
|
||||
node.dataset.src = src;
|
||||
if (node.nodeName === "IMG") {
|
||||
node.removeAttribute("src");
|
||||
} else {
|
||||
node.setAttribute("src", "about:blank");
|
||||
}
|
||||
node.dispatchEvent(new Event("load"));
|
||||
}
|
||||
}
|
||||
}).observe(document.body, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributeFilter: ["src"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ QUnit.test("can be rendered", async (assert) => {
|
||||
let userMenuEl = userMenu.el;
|
||||
assert.containsOnce(userMenuEl, "img.o_user_avatar");
|
||||
assert.strictEqual(
|
||||
userMenuEl.querySelector("img.o_user_avatar").src,
|
||||
userMenuEl.querySelector("img.o_user_avatar").dataset.src,
|
||||
"http://lordofthering/web/image?model=res.users&field=avatar_128&id=7"
|
||||
);
|
||||
assert.containsOnce(userMenuEl, "span.oe_topbar_name");
|
||||
|
||||
Reference in New Issue
Block a user