import { addLink, parseAndTransform } from "@mail/utils/common/format"; import { useSequential } from "@mail/utils/common/hooks"; import { click, contains, defineMailModels, insertText, openDiscuss, start, startServer, } from "./mail_test_helpers"; import { describe, expect, test } from "@odoo/hoot"; import { markup } from "@odoo/owl"; describe.current.tags("desktop"); defineMailModels(); test("add_link utility function", () => { const testInputs = { "http://admin:password@example.com:8/%2020": true, "https://admin:password@example.com/test": true, "www.example.com:8/test": true, "https://127.0.0.5:8069": true, "www.127.0.0.5": false, "should.notmatch": false, "fhttps://test.example.com/test": false, "https://www.transifex.com/odoo/odoo-11/translate/#fr/lunch?q=text%3A'La+Tartiflette'": true, "https://www.transifex.com/odoo/odoo-11/translate/#fr/$/119303430?q=text%3ATartiflette": true, "https://tenor.com/view/chỗgiặt-dog-smile-gif-13860250": true, "http://www.boîtenoire.be": true, "https://github.com/odoo/enterprise/compare/16.0...odoo-dev:enterprise:16.0-voip-fix_demo_data-tsm?expand=1": true, "https://github.com/odoo/enterprise/compare/16.0...16.0-voip-fix_demo_data-tsm?expand=1": true, "https://github.com/odoo/enterprise/compare/16.0...chỗgiặt-voip-fix_demo_data-tsm?expand=1": true, "https://github.com/odoo/enterprise/compare/chỗgiặt...chỗgiặt-voip-fix_demo_data-tsm?expand=1": true, "https://github.com/odoo/enterprise/compare/@...}-voip-fix_demo_data-tsm?expand=1": true, "https://x.com": true, }; for (const [content, willLinkify] of Object.entries(testInputs)) { const output = parseAndTransform(content, addLink); if (willLinkify) { expect(output.indexOf("")).toBe(output.length - 4); } else { expect(output.indexOf(" { const testInputs = [ // textContent not unescaped [ markup("

https://example.com/?&currency_id

"), '

https://example.com/?&currency_id

', ], // entities not unescaped [markup("& &amp; > <"), "& &amp; > <"], // > and " not linkified since they are not in URL regex [ markup("

https://example.com/>

"), '

https://example.com/>

', ], [ markup('

https://example.com/"hello">

'), '

https://example.com/"hello">

', ], // & and ' linkified since they are in URL regex [ markup("

https://example.com/&hello

"), '

https://example.com/&hello

', ], [ markup("

https://example.com/'yeah'

"), '

https://example.com/\'yeah\'

', ], [markup("

:'(

"), "

:'(

"], [markup(":'("), ":'("], ["

:'(

", "<p>:'(</p>"], [":'(", ":'("], [markup("<3"), "<3"], [markup("<3"), "<3"], ["<3", "<3"], // Already encoded url should not be encoded twice [ markup("https://odoo.com/%5B%5D"), `https://odoo.com/[]`, ], ]; for (const [content, result] of testInputs) { const output = parseAndTransform(content, addLink); expect(output).toBeInstanceOf(markup().constructor); expect(output.toString()).toBe(result); } }); test("addLink: linkify inside text node (1 occurrence)", async () => { const content = markup("

some text https://somelink.com

"); const linkified = parseAndTransform(content, addLink); expect(linkified.startsWith("

some text

")).toBe(true); // linkify may add some attributes. Since we do not care of their exact // stringified representation, we continue deeper assertion with query // selectors. const fragment = document.createDocumentFragment(); const div = document.createElement("div"); fragment.appendChild(div); div.innerHTML = linkified; expect(div).toHaveText("some text https://somelink.com"); await contains("a", { target: div }); expect(div.querySelector(":scope a")).toHaveText("https://somelink.com"); }); test("addLink: linkify inside text node (2 occurrences)", () => { // linkify may add some attributes. Since we do not care of their exact // stringified representation, we continue deeper assertion with query // selectors. const content = markup( "

some text https://somelink.com and again https://somelink2.com ...

" ); const linkified = parseAndTransform(content, addLink); const fragment = document.createDocumentFragment(); const div = document.createElement("div"); fragment.appendChild(div); div.innerHTML = linkified; expect(div).toHaveText("some text https://somelink.com and again https://somelink2.com ..."); expect(div.querySelectorAll(":scope a")).toHaveCount(2); expect(div.querySelectorAll(":scope a")[0]).toHaveText("https://somelink.com"); expect(div.querySelectorAll(":scope a")[1]).toHaveText("https://somelink2.com"); }); test("url", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); await start(); await openDiscuss(channelId); // see: https://www.ietf.org/rfc/rfc1738.txt const messageBody = "https://odoo.com?test=~^|`{}[]#"; await insertText(".o-mail-Composer-input", messageBody); await click("button[aria-label='Send']:enabled"); await contains(`.o-mail-Message a:contains(${messageBody})`); }); test("url with comma at the end", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); await start(); await openDiscuss(channelId); const messageBody = "Go to https://odoo.com, it's great!"; await insertText(".o-mail-Composer-input", messageBody); await click("button[aria-label='Send']:enabled"); await contains(".o-mail-Message a:contains(https://odoo.com)"); await contains(`.o-mail-Message-content:contains(${messageBody}`); }); test("url with dot at the end", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); await start(); await openDiscuss(channelId); const messageBody = "Go to https://odoo.com. It's great!"; await insertText(".o-mail-Composer-input", messageBody); await click("button[aria-label='Send']:enabled"); await contains(".o-mail-Message a:contains(https://odoo.com)"); await contains(`.o-mail-Message-content:contains(${messageBody})`); }); test("url with semicolon at the end", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); await start(); await openDiscuss(channelId); const messageBody = "Go to https://odoo.com; it's great!"; await insertText(".o-mail-Composer-input", messageBody); await click("button[aria-label='Send']:enabled"); await contains(".o-mail-Message a:contains(https://odoo.com)"); await contains(`.o-mail-Message-content:contains(${messageBody})`); }); test("url with ellipsis at the end", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); await start(); await openDiscuss(channelId); const messageBody = "Go to https://odoo.com... it's great!"; await insertText(".o-mail-Composer-input", messageBody); await click("button[aria-label='Send']:enabled"); await contains(".o-mail-Message a:contains(https://odoo.com)"); await contains(`.o-mail-Message-content:contains(${messageBody})`); }); test("url with number in subdomain", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); await start(); await openDiscuss(channelId); const messageBody = "https://www.45017478-master-all.runbot134.odoo.com/odoo"; await insertText(".o-mail-Composer-input", messageBody); await click("button[aria-label='Send']:enabled"); await contains( ".o-mail-Message a:contains(https://www.45017478-master-all.runbot134.odoo.com/odoo)" ); }); test("isSequential doesn't execute intermediate call.", async () => { const sequential = useSequential(); let index = 0; const sequence = () => { index++; const i = index; return sequential(async () => { expect.step(i.toString()); return new Promise((r) => setTimeout(() => r(i), 1)); }); }; const result = await Promise.all([sequence(), sequence(), sequence(), sequence(), sequence()]); expect(result).toEqual([1, undefined, undefined, undefined, 5]); expect.verifySteps(["1", "5"]); });