diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 30d120144ea..89be1e48ca3 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -152,7 +152,9 @@ For more specific needs, you may also assign custom-defined actions # scss not needed in tests and depends on scss variables that are not in this bundle ('remove', 'web_tour/static/src/tour_pointer/**/*.scss'), 'web_tour/static/src/tour_service/**/*', - 'web/static/tests/legacy/helpers/test_utils_file.js', + 'web/static/tests/helpers/cleanup.js', + 'web/static/tests/helpers/utils.js', + 'web/static/tests/utils.js', 'mail/static/tests/tours/discuss_public_tour.js', 'mail/static/tests/tours/discuss_channel_as_guest_tour.js', ], diff --git a/addons/mail/static/tests/thread/file_upload_tests.js b/addons/mail/static/tests/thread/file_upload_tests.js index 5393de86a86..b00d8860c46 100644 --- a/addons/mail/static/tests/thread/file_upload_tests.js +++ b/addons/mail/static/tests/thread/file_upload_tests.js @@ -4,11 +4,7 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; import { start } from "@mail/../tests/helpers/test_utils"; -import { editInput } from "@web/../tests/helpers/utils"; -import { file } from "@web/../tests/legacy/helpers/test_utils"; -import { click, contains } from "@web/../tests/utils"; - -const { createFile } = file; +import { click, contains, createFile, inputFiles } from "@web/../tests/utils"; QUnit.module("file upload"); @@ -29,23 +25,23 @@ QUnit.test("no conflicts between file uploads", async () => { views: [[false, "form"]], }); await click("button", { text: "Send message" }); - const file1 = await createFile({ - name: "text1.txt", - content: "hello, world", - contentType: "text/plain", - }); - await contains(".o-mail-Chatter .o-mail-Composer input[type=file]"); - editInput(document.body, ".o-mail-Chatter .o-mail-Composer input[type=file]", file1); + await inputFiles(".o-mail-Chatter .o-mail-Composer input[type=file]", [ + await createFile({ + name: "text1.txt", + content: "hello, world", + contentType: "text/plain", + }), + ]); // Uploading file in the second thread: discuss.channel in chatWindow. await click("i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); - const file2 = await createFile({ - name: "text2.txt", - content: "hello, world", - contentType: "text/plain", - }); - await contains(".o-mail-ChatWindow .o-mail-Composer input[type=file]"); - editInput(document.body, ".o-mail-ChatWindow input[type=file]", file2); + await inputFiles(".o-mail-ChatWindow .o-mail-Composer input[type=file]", [ + await createFile({ + name: "text2.txt", + content: "hello, world", + contentType: "text/plain", + }), + ]); await contains(".o-mail-Chatter .o-mail-AttachmentCard"); await contains(".o-mail-ChatWindow .o-mail-AttachmentCard"); }); @@ -62,11 +58,12 @@ QUnit.test("Attachment shows spinner during upload", async () => { }, }); await openDiscuss(channelId); - const file = await createFile({ - name: "text2.txt", - content: "hello, world", - contentType: "text/plain", - }); - editInput(document.body, ".o-mail-Composer input[type=file]", file); + await inputFiles(".o-mail-Composer input[type=file]", [ + await createFile({ + name: "text2.txt", + content: "hello, world", + contentType: "text/plain", + }), + ]); await contains(".o-mail-AttachmentCard .fa-spinner"); }); diff --git a/addons/mail/static/tests/tours/discuss_public_tour.js b/addons/mail/static/tests/tours/discuss_public_tour.js index 61898474e95..ee2cfc7967a 100644 --- a/addons/mail/static/tests/tours/discuss_public_tour.js +++ b/addons/mail/static/tests/tours/discuss_public_tour.js @@ -1,7 +1,7 @@ /* @odoo-module */ import { registry } from "@web/core/registry"; -import { createFile, inputFiles } from "@web/../tests/legacy/helpers/test_utils_file"; +import { createFile, inputFiles } from "@web/../tests/utils"; registry.category("web_tour.tours").add("mail/static/tests/tours/discuss_public_tour.js", { test: true, @@ -43,13 +43,12 @@ registry.category("web_tour.tours").add("mail/static/tests/tours/discuss_public_ content: "Add one file in composer", trigger: ".o-mail-Composer button[aria-label='Attach files']", async run() { - const file = await createFile({ - content: "hello, world", - contentType: "text/plain", - name: "text.txt", - }); - inputFiles(document.querySelector(".o-mail-Composer-coreMain .o_input_file"), [ - file, + await inputFiles(".o-mail-Composer-coreMain .o_input_file", [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), ]); }, }, @@ -90,17 +89,13 @@ registry.category("web_tour.tours").add("mail/static/tests/tours/discuss_public_ content: "Add one more file in composer", trigger: ".o-mail-Message .o-mail-Composer button[aria-label='Attach files']", async run() { - const file = await createFile({ - content: "hello 2", - contentType: "text/plain", - name: "extra.txt", - }); - inputFiles( - document.querySelector( - ".o-mail-Message .o-mail-Composer-coreMain .o_input_file" - ), - [file] - ); + inputFiles(".o-mail-Message .o-mail-Composer-coreMain .o_input_file", [ + await createFile({ + content: "hello 2", + contentType: "text/plain", + name: "extra.txt", + }), + ]); }, }, { diff --git a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js index 0b1a3b913c3..3e3938ebef9 100644 --- a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js +++ b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js @@ -1,7 +1,7 @@ /* @odoo-module */ import { registry } from "@web/core/registry"; -import { createFile, inputFiles } from "@web/../tests/legacy/helpers/test_utils_file"; +import { createFile, inputFiles } from "@web/../tests/utils"; /** * This tour depends on data created by python test in charge of launching it. @@ -25,13 +25,12 @@ registry.category("web_tour.tours").add("mail/static/tests/tours/mail_full_compo content: "Add one file in composer", trigger: ".o-mail-Composer button[aria-label='Attach files']", async run() { - const file = await createFile({ - content: "hello, world", - contentType: "text/plain", - name: "text.txt", - }); - inputFiles(document.querySelector(".o-mail-Composer-coreMain .o_input_file"), [ - file, + await inputFiles(".o-mail-Composer-coreMain .o_input_file", [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), ]); }, }, diff --git a/addons/mail/static/tests/web/chatter_tests.js b/addons/mail/static/tests/web/chatter_tests.js index 64819ab02c9..65b649ca1c0 100644 --- a/addons/mail/static/tests/web/chatter_tests.js +++ b/addons/mail/static/tests/web/chatter_tests.js @@ -7,18 +7,16 @@ import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; import { start } from "@mail/../tests/helpers/test_utils"; import { triggerHotkey } from "@web/../tests/helpers/utils"; -import { file } from "@web/../tests/legacy/helpers/test_utils"; import { click, contains, + createFile, dragenterFiles, dropFiles, insertText, scroll, } from "@web/../tests/utils"; -const { createFile } = file; - QUnit.module("chatter"); QUnit.test("simple chatter on a record", async (assert) => { diff --git a/addons/mrp/static/tests/mrp_document_kanban_tests.js b/addons/mrp/static/tests/mrp_document_kanban_tests.js index 8ed15e1ed87..42d6c2cb123 100644 --- a/addons/mrp/static/tests/mrp_document_kanban_tests.js +++ b/addons/mrp/static/tests/mrp_document_kanban_tests.js @@ -7,9 +7,8 @@ import { start } from "@mail/../tests/helpers/test_utils"; import { fileUploadService } from "@web/core/file_upload/file_upload_service"; import { registry } from "@web/core/registry"; -import testUtils from "@web/../tests/legacy/helpers/test_utils"; import { getFixture, nextTick } from "@web/../tests/helpers/utils"; -import { click, contains } from "@web/../tests/utils"; +import { click, contains, createFile, inputFiles } from "@web/../tests/utils"; import { setupViewRegistries } from "@web/../tests/views/helpers"; addModelNamesToFetch(["mrp.document"]); @@ -85,24 +84,6 @@ QUnit.module('MrpDocumentsKanbanView', { }); QUnit.test('mrp: upload multiple files', async function (assert) { - assert.expect(4); - - const file1 = await testUtils.file.createFile({ - name: 'text1.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - const file2 = await testUtils.file.createFile({ - name: 'text2.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - const file3 = await testUtils.file.createFile({ - name: 'text3.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - const mockedXHRs = []; this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend')); @@ -122,31 +103,30 @@ QUnit.module('MrpDocumentsKanbanView', { views: [[false, 'kanban']], }); - const fileInput = target.querySelector(".o_input_file"); - - let dataTransfer = new DataTransfer(); - dataTransfer.items.add(file1); - fileInput.files = dataTransfer.files; - fileInput.dispatchEvent(new Event('change', { bubbles: true })); - assert.verifySteps(['xhrSend']); - - dataTransfer = new DataTransfer(); - dataTransfer.items.add(file2); - dataTransfer.items.add(file3); - fileInput.files = dataTransfer.files; - fileInput.dispatchEvent(new Event('change', { bubbles: true })); - assert.verifySteps(['xhrSend']); + await inputFiles(".o_control_panel_collapsed_create .o_input_file", [ + await createFile({ + name: "text1.txt", + content: "hello, world", + contentType: "text/plain", + }), + ]); + assert.verifySteps(["xhrSend"]); + await inputFiles(".o_control_panel_collapsed_create .o_input_file", [ + await createFile({ + name: "text2.txt", + content: "hello, world", + contentType: "text/plain", + }), + await createFile({ + name: "text3.txt", + content: "hello, world", + contentType: "text/plain", + }), + ]); + assert.verifySteps(["xhrSend"]); }); QUnit.test('mrp: upload progress bars', async function (assert) { - assert.expect(4); - - const file1 = await testUtils.file.createFile({ - name: 'text1.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - const mockedXHRs = []; this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend')); @@ -166,13 +146,14 @@ QUnit.module('MrpDocumentsKanbanView', { views: [[false, 'kanban']], }); - const fileInput = target.querySelector(".o_input_file"); - - let dataTransfer = new DataTransfer(); - dataTransfer.items.add(file1); - fileInput.files = dataTransfer.files; - fileInput.dispatchEvent(new Event('change', { bubbles: true })); - assert.verifySteps(['xhrSend']); + await inputFiles(".o_control_panel_collapsed_create .o_input_file", [ + await createFile({ + name: "text1.txt", + content: "hello, world", + contentType: "text/plain", + }), + ]); + assert.verifySteps(["xhrSend"]); const progressEvent = new Event('progress', { bubbles: true }); progressEvent.loaded = 250000000; diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 2f01c695860..e4f82818899 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -428,7 +428,9 @@ This module provides the core of the Odoo Web Client. 'web.assets_tests': [ # No tours are defined in web, but the bundle "assets_tests" is # first called in web. - 'web/static/tests/legacy/helpers/test_utils_file.js' + 'web/static/tests/helpers/cleanup.js', + 'web/static/tests/helpers/utils.js', + 'web/static/tests/utils.js', ], 'web.__assets_tests_call__': [ 'web/static/tests/ignore_missing_deps_start.js', diff --git a/addons/web/static/tests/helpers/cleanup.js b/addons/web/static/tests/helpers/cleanup.js index c48e92ead5d..6fe62d8128a 100644 --- a/addons/web/static/tests/helpers/cleanup.js +++ b/addons/web/static/tests/helpers/cleanup.js @@ -19,93 +19,97 @@ export function registerCleanup(callback) { cleanups.push(callback); } -QUnit.on("OdooAfterTestHook", (info) => { - if (QUnit.config.debug) { - return; - } - let cleanup; - // note that this calls the cleanup callbacks in reverse order! - while ((cleanup = cleanups.pop())) { - try { - cleanup(info); - } catch (error) { - console.error(error); +if (window.QUnit) { + QUnit.on("OdooAfterTestHook", (info) => { + if (QUnit.config.debug) { + return; } - } -}); - -// ----------------------------------------------------------------------------- -// Check leftovers -// ----------------------------------------------------------------------------- - -/** - * List of elements tolerated in the body after a test. The property "keep" - * prevents the element from being removed (typically: qunit suite elements). - */ -const validElements = [ - // always in the body: - { tagName: "DIV", attr: "id", value: "qunit", keep: true }, - { tagName: "DIV", attr: "id", value: "qunit-fixture", keep: true }, - // shouldn't be in the body after a test but are tolerated: - { tagName: "SCRIPT", attr: "id", value: "" }, - { tagName: "DIV", attr: "class", value: "o_notification_manager" }, - { tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto" }, - { tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto show" }, - { tagName: "DIV", attr: "class", value: "tooltip tooltip-field-info fade bs-tooltip-auto" }, - { - tagName: "DIV", - attr: "class", - value: "tooltip tooltip-field-info fade bs-tooltip-auto show", - }, - { tagName: "SPAN", attr: "class", value: "select2-hidden-accessible" }, - - // Due to a Document Kanban bug (already present in 12.0) - { tagName: "DIV", attr: "class", value: "ui-helper-hidden-accessible" }, - { - tagName: "UL", - attr: "class", - value: "ui-menu ui-widget ui-widget-content ui-autocomplete ui-front", - }, - { - tagName: "UL", - attr: "class", - value: "ui-menu ui-widget ui-widget-content ui-autocomplete dropdown-menu ui-front", // many2ones - }, -]; - -/** - * After each test, we check that there is no leftover in the DOM. - * - * Note: this event is not QUnit standard, we added it for this specific use case. - * As a payload, an object with keys 'moduleName' and 'testName' is provided. It - * is used to indicate the test that left elements in the DOM, when it happens. - */ -QUnit.on("OdooAfterTestHook", function (info) { - if (QUnit.config.debug) { - return; - } - const failed = info.testReport.getStatus() === "failed"; - const toRemove = []; - // check for leftover elements in the body - for (const bodyChild of document.body.children) { - const tolerated = validElements.find( - (e) => e.tagName === bodyChild.tagName && bodyChild.getAttribute(e.attr) === e.value - ); - if (!failed && !tolerated) { - QUnit.pushFailure(`Body still contains undesirable elements:\n${bodyChild.outerHTML}`); + let cleanup; + // note that this calls the cleanup callbacks in reverse order! + while ((cleanup = cleanups.pop())) { + try { + cleanup(info); + } catch (error) { + console.error(error); + } } - if (!tolerated || !tolerated.keep) { - toRemove.push(bodyChild); + }); + + // ----------------------------------------------------------------------------- + // Check leftovers + // ----------------------------------------------------------------------------- + + /** + * List of elements tolerated in the body after a test. The property "keep" + * prevents the element from being removed (typically: qunit suite elements). + */ + const validElements = [ + // always in the body: + { tagName: "DIV", attr: "id", value: "qunit", keep: true }, + { tagName: "DIV", attr: "id", value: "qunit-fixture", keep: true }, + // shouldn't be in the body after a test but are tolerated: + { tagName: "SCRIPT", attr: "id", value: "" }, + { tagName: "DIV", attr: "class", value: "o_notification_manager" }, + { tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto" }, + { tagName: "DIV", attr: "class", value: "tooltip fade bs-tooltip-auto show" }, + { tagName: "DIV", attr: "class", value: "tooltip tooltip-field-info fade bs-tooltip-auto" }, + { + tagName: "DIV", + attr: "class", + value: "tooltip tooltip-field-info fade bs-tooltip-auto show", + }, + { tagName: "SPAN", attr: "class", value: "select2-hidden-accessible" }, + + // Due to a Document Kanban bug (already present in 12.0) + { tagName: "DIV", attr: "class", value: "ui-helper-hidden-accessible" }, + { + tagName: "UL", + attr: "class", + value: "ui-menu ui-widget ui-widget-content ui-autocomplete ui-front", + }, + { + tagName: "UL", + attr: "class", + value: "ui-menu ui-widget ui-widget-content ui-autocomplete dropdown-menu ui-front", // many2ones + }, + ]; + + /** + * After each test, we check that there is no leftover in the DOM. + * + * Note: this event is not QUnit standard, we added it for this specific use case. + * As a payload, an object with keys 'moduleName' and 'testName' is provided. It + * is used to indicate the test that left elements in the DOM, when it happens. + */ + QUnit.on("OdooAfterTestHook", function (info) { + if (QUnit.config.debug) { + return; } - } - // cleanup leftovers in #qunit-fixture - const qunitFixture = document.getElementById("qunit-fixture"); - if (qunitFixture.children.length) { - toRemove.push(...qunitFixture.children); - } - // remove unwanted elements if not in debug - for (const el of toRemove) { - el.remove(); - } - document.body.classList.remove("modal-open"); -}); + const failed = info.testReport.getStatus() === "failed"; + const toRemove = []; + // check for leftover elements in the body + for (const bodyChild of document.body.children) { + const tolerated = validElements.find( + (e) => e.tagName === bodyChild.tagName && bodyChild.getAttribute(e.attr) === e.value + ); + if (!failed && !tolerated) { + QUnit.pushFailure( + `Body still contains undesirable elements:\n${bodyChild.outerHTML}` + ); + } + if (!tolerated || !tolerated.keep) { + toRemove.push(bodyChild); + } + } + // cleanup leftovers in #qunit-fixture + const qunitFixture = document.getElementById("qunit-fixture"); + if (qunitFixture.children.length) { + toRemove.push(...qunitFixture.children); + } + // remove unwanted elements if not in debug + for (const el of toRemove) { + el.remove(); + } + document.body.classList.remove("modal-open"); + }); +} diff --git a/addons/web/static/tests/helpers/utils.js b/addons/web/static/tests/helpers/utils.js index 8c14528d550..7115da157bf 100644 --- a/addons/web/static/tests/helpers/utils.js +++ b/addons/web/static/tests/helpers/utils.js @@ -146,6 +146,9 @@ export function patchWithCleanup(obj, patchValue) { * @returns {Element} */ export function getFixture() { + if (!window.QUnit) { + return document; + } if (QUnit.config.debug) { return document.body; } else { diff --git a/addons/web/static/tests/legacy/helpers/test_utils.js b/addons/web/static/tests/legacy/helpers/test_utils.js index 899354a039d..e4932b6020e 100644 --- a/addons/web/static/tests/legacy/helpers/test_utils.js +++ b/addons/web/static/tests/legacy/helpers/test_utils.js @@ -9,7 +9,6 @@ import testUtilsDom from "@web/../tests/legacy/helpers/test_utils_dom"; import testUtilsFields from "@web/../tests/legacy/helpers/test_utils_fields"; - import testUtilsFile from "@web/../tests/legacy/helpers/test_utils_file"; import testUtilsMock from "@web/../tests/legacy/helpers/test_utils_mock"; function deprecated(fn, type) { @@ -107,19 +106,10 @@ triggerKeydown: testUtilsFields.triggerKeydown, }; - export const file = { - createFile: testUtilsFile.createFile, - dragoverFile: testUtilsFile.dragoverFile, - dropFile: testUtilsFile.dropFile, - dropFiles: testUtilsFile.dropFiles, - inputFiles: testUtilsFile.inputFiles, - }; - export default { mock, dom, fields, - file, makeTestPromise: makeTestPromise, makeTestPromiseWithAssert: makeTestPromiseWithAssert, diff --git a/addons/web/static/tests/legacy/helpers/test_utils_file.js b/addons/web/static/tests/legacy/helpers/test_utils_file.js deleted file mode 100644 index 7fa1c0fb2e1..00000000000 --- a/addons/web/static/tests/legacy/helpers/test_utils_file.js +++ /dev/null @@ -1,155 +0,0 @@ -/** @odoo-module **/ - -/** - * FILE Test Utils - * - * This module defines various utility functions to help simulate events with - * files, such as drag-and-drop. - * - * Note that all methods defined in this module are exported in the main - * testUtils file. - */ - - -//------------------------------------------------------------------------------ -// Private functions -//------------------------------------------------------------------------------ - -/** - * Create a fake object 'dataTransfer', linked to some files, which is passed to - * drag and drop events. - * - * @param {Object[]} files - * @returns {Object} - */ -function _createFakeDataTransfer(files) { - return { - dropEffect: 'all', - effectAllowed: 'all', - files, - getData: function () { - return files; - }, - items: [], - types: ['Files'], - }; -} - -//------------------------------------------------------------------------------ -// Public functions -//------------------------------------------------------------------------------ - -/** - * Create a file object, which can be used for drag-and-drop. - * - * @param {Object} data - * @param {string} data.name - * @param {string} data.content - * @param {string} data.contentType - * @returns {Promise} resolved with file created - */ -export function createFile(data) { - // Note: this is only supported by Chrome, and does not work in Incognito mode - return new Promise(function (resolve, reject) { - var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; - if (!requestFileSystem) { - throw new Error('FileSystem API is not supported'); - } - requestFileSystem(window.TEMPORARY, 1024 * 1024, function (fileSystem) { - fileSystem.root.getFile(data.name, { create: true }, function (fileEntry) { - fileEntry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function (e) { - fileSystem.root.getFile(data.name, {}, function (fileEntry) { - fileEntry.file(function (file) { - resolve(file); - }); - }); - }; - fileWriter.write(new Blob([ data.content ], { type: data.contentType })); - }); - }); - }); - }); -} - -/** - * Drag a file over a DOM element - * - * @param {$.Element} $el - * @param {Object} file must have been created beforehand (@see createFile) - */ -function dragoverFile($el, file) { - var ev = new Event('dragover', { bubbles: true }); - Object.defineProperty(ev, 'dataTransfer', { - value: _createFakeDataTransfer(file), - }); - $el[0].dispatchEvent(ev); -} - -/** - * Drop a file on a DOM element. - * - * @param {$.Element} $el - * @param {Object} file must have been created beforehand (@see createFile) - */ -function dropFile($el, file) { - var ev = new Event('drop', { bubbles: true, }); - Object.defineProperty(ev, 'dataTransfer', { - value: _createFakeDataTransfer([file]), - }); - $el[0].dispatchEvent(ev); -} - -/** - * Drop some files on a DOM element. - * - * @param {$.Element} $el - * @param {Object[]} files must have been created beforehand (@see createFile) - */ -function dropFiles($el, files) { - var ev = new Event('drop', { bubbles: true, }); - Object.defineProperty(ev, 'dataTransfer', { - value: _createFakeDataTransfer(files), - }); - $el[0].dispatchEvent(ev); -} - -/** - * Set files in a file input - * - * @param {DOM.Element} el - * @param {Object[]} files must have been created beforehand - * @see testUtils.file.createFile - */ -export function inputFiles(el, files) { - // could not use _createFakeDataTransfer as el.files assignation will only - // work with a real FileList object. - const dataTransfer = new window.DataTransfer(); - for (const file of files) { - dataTransfer.items.add(file); - } - el.files = dataTransfer.files; - /** - * Changing files programatically is not supposed to trigger the event but - * it does in Chrome versions before 73 (which is on runbot), so in that - * case there is no need to make a manual dispatch, because it would lead to - * the files being added twice. - */ - const versionRaw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - const chromeVersion = versionRaw ? parseInt(versionRaw[2], 10) : false; - if (!chromeVersion || chromeVersion >= 73) { - el.dispatchEvent(new Event('change')); - } -} - -//------------------------------------------------------------------------------ -// Exposed API -//------------------------------------------------------------------------------ - -export default { - createFile: createFile, - dragoverFile: dragoverFile, - dropFile: dropFile, - dropFiles, - inputFiles, -}; diff --git a/addons/web/static/tests/utils.js b/addons/web/static/tests/utils.js index 77f4bdd5188..4179a774883 100644 --- a/addons/web/static/tests/utils.js +++ b/addons/web/static/tests/utils.js @@ -8,12 +8,39 @@ import { makeDeferred, triggerEvents as webTriggerEvents, } from "@web/../tests/helpers/utils"; -import { file } from "@web/../tests/legacy/helpers/test_utils"; -import { createFile as createFile2 } from "@web/../tests/legacy/helpers/test_utils_file"; -const { inputFiles: webInputFiles } = file; - -export const createFile = createFile2; +/** + * Create a file object, which can be used for drag-and-drop. + * + * @param {Object} data + * @param {string} data.name + * @param {string} data.content + * @param {string} data.contentType + * @returns {Promise} resolved with file created + */ +export function createFile(data) { + // Note: this is only supported by Chrome, and does not work in Incognito mode + return new Promise(function (resolve, reject) { + var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; + if (!requestFileSystem) { + throw new Error("FileSystem API is not supported"); + } + requestFileSystem(window.TEMPORARY, 1024 * 1024, function (fileSystem) { + fileSystem.root.getFile(data.name, { create: true }, function (fileEntry) { + fileEntry.createWriter(function (fileWriter) { + fileWriter.onwriteend = function (e) { + fileSystem.root.getFile(data.name, {}, function (fileEntry) { + fileEntry.file(function (file) { + resolve(file); + }); + }); + }; + fileWriter.write(new Blob([data.content], { type: data.contentType })); + }); + }); + }); + }); +} /** * Create a fake object 'dataTransfer', linked to some files, @@ -48,7 +75,7 @@ export async function click(selector, options = {}) { /** * Waits until exactly one element matching the given `selector` is present in - * `options.target` and then drags `files` on it. + * `options.target` and then dragenters `files` on it. * * @param {string} selector * @param {Object[]} files @@ -58,6 +85,18 @@ export async function dragenterFiles(selector, files, options) { await contains(selector, { dragenterFiles: files, ...options }); } +/** + * Waits until exactly one element matching the given `selector` is present in + * `options.target` and then dragovers `files` on it. + * + * @param {string} selector + * @param {Object[]} files + * @param {ContainsOptions} [options] forwarded to `contains` + */ +export async function dragoverFiles(selector, files, options) { + await contains(selector, { dragoverFiles: files, ...options }); +} + /** * Waits until exactly one element matching the given `selector` is present in * `options.target` and then drops `files` on it. @@ -144,8 +183,22 @@ export async function triggerEvents(selector, events, options) { await contains(selector, { triggerEvents: events, ...options }); } +function log(ok, message) { + if (window.QUnit) { + QUnit.assert.ok(ok, message); + } else { + if (ok) { + console.log(message); + } else { + console.error(message); + } + } +} + let hasUsedContainsPositively = false; -QUnit.testStart(() => (hasUsedContainsPositively = false)); +if (window.QUnit) { + QUnit.testStart(() => (hasUsedContainsPositively = false)); +} /** * @typedef {Object} ContainsOptions * @property {[string, ContainsOptions]} [after] if provided, the found element(s) must be after the @@ -161,7 +214,8 @@ QUnit.testStart(() => (hasUsedContainsPositively = false)); * multiple rules that have to match at the same time. * @property {number} [count=1] numbers of elements to be found to declare the contains check * as successful. Elements are counted after applying all other filters. - * @property {Object[]} [dragenterFiles] if provided, drags the given files on the found element + * @property {Object[]} [dragenterFiles] if provided, dragenters the given files on the found element + * @property {Object[]} [dragoverFiles] if provided, dragovers the given files on the found element * @property {Object[]} [dropFiles] if provided, drops the given files on the found element * @property {Object[]} [inputFiles] if provided, inputs the given files on the found element * @property {{content:string, replace:boolean}} [insertText] if provided, adds to (or replace) the @@ -322,16 +376,16 @@ class Contains { : `${message} Parent not found.`; if (this.parentContains) { if (this.parentContains.successMessage) { - QUnit.assert.ok(true, this.parentContains.successMessage); + log(true, this.parentContains.successMessage); } else { this.parentContains.executeError(); } } - QUnit.assert.ok(false, message); + log(false, message); this.def?.reject(new Error(message)); for (const childContains of this.childrenContains || []) { if (childContains.successMessage) { - QUnit.assert.ok(true, childContains.successMessage); + log(true, childContains.successMessage); } else { childContains.executeError(); } @@ -360,13 +414,21 @@ class Contains { }); } if (this.options.dragenterFiles) { - message = `${message} and dragged ${this.options.dragenterFiles.length} file(s)`; + message = `${message} and dragentered ${this.options.dragenterFiles.length} file(s)`; const ev = new Event("dragenter", { bubbles: true }); Object.defineProperty(ev, "dataTransfer", { value: createFakeDataTransfer(this.options.dragenterFiles), }); el.dispatchEvent(ev); } + if (this.options.dragoverFiles) { + message = `${message} and dragovered ${this.options.dragoverFiles.length} file(s)`; + const ev = new Event("dragover", { bubbles: true }); + Object.defineProperty(ev, "dataTransfer", { + value: createFakeDataTransfer(this.options.dragoverFiles), + }); + el.dispatchEvent(ev); + } if (this.options.dropFiles) { message = `${message} and dropped ${this.options.dropFiles.length} file(s)`; const ev = new Event("drop", { bubbles: true }); @@ -377,7 +439,24 @@ class Contains { } if (this.options.inputFiles) { message = `${message} and inputted ${this.options.inputFiles.length} file(s)`; - webInputFiles(el, this.options.inputFiles); + // could not use _createFakeDataTransfer as el.files assignation will only + // work with a real FileList object. + const dataTransfer = new window.DataTransfer(); + for (const file of this.options.inputFiles) { + dataTransfer.items.add(file); + } + el.files = dataTransfer.files; + /** + * Changing files programatically is not supposed to trigger the event but + * it does in Chrome versions before 73 (which is on runbot), so in that + * case there is no need to make a manual dispatch, because it would lead to + * the files being added twice. + */ + const versionRaw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + const chromeVersion = versionRaw ? parseInt(versionRaw[2], 10) : false; + if (!chromeVersion || chromeVersion >= 73) { + el.dispatchEvent(new Event("change")); + } } if (this.options.insertText !== undefined) { message = `${message} and inserted text "${this.options.insertText.content}" (replace: ${this.options.insertText.replace})`; @@ -421,11 +500,11 @@ class Contains { }); } if (this.parentContains) { - QUnit.assert.ok(true, this.parentContains.successMessage); + log(true, this.parentContains.successMessage); } - QUnit.assert.ok(true, message); + log(true, message); for (const childContains of this.childrenContains) { - QUnit.assert.ok(true, childContains.successMessage); + log(true, childContains.successMessage); } this.def?.resolve(); } diff --git a/odoo/tools/js_transpiler.py b/odoo/tools/js_transpiler.py index fbdf4653db5..3feea06efa5 100644 --- a/odoo/tools/js_transpiler.py +++ b/odoo/tools/js_transpiler.py @@ -101,7 +101,7 @@ def wrap_with_qunit_module(url, content): """ Wraps the test file content (source code) with the QUnit.module('module_name', function() {...}). """ - if "tests" in url and re.search(r'QUnit\.(test|debug|only)', content): + if "tests" in url and re.search(r'QUnit\.(test|debug|only)\(', content): match = URL_RE.match(url) return f"""QUnit.module("{match["module"]}", function() {{{content}}});""" else: