This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/tests/integration/components/d-editor-test.js
Joffrey JAFFEUX bcb89beb99
A11Y: makes toolbar tabindex independent from its context (#17889)
Prior to this fix, if `<DEditor />` was used in a context where quote would not be the first button, then no  button would be focusable.
2022-08-12 15:24:20 -04:00

1057 lines
32 KiB
JavaScript

import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { click, fillIn, render, settled } from "@ember/test-helpers";
import {
chromeTest,
exists,
paste,
query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import {
getTextareaSelection,
setTextareaSelection,
} from "discourse/tests/helpers/textarea-selection-helper";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
import formatTextWithSelection from "discourse/tests/helpers/d-editor-helper";
import { next } from "@ember/runloop";
import { withPluginApi } from "discourse/lib/plugin-api";
module("Integration | Component | d-editor", function (hooks) {
setupRenderingTest(hooks);
test("preview updates with markdown", async function (assert) {
await render(hbs`<DEditor @value={{this.value}} />`);
assert.ok(exists(".d-editor-button-bar"));
await fillIn(".d-editor-input", "hello **world**");
assert.strictEqual(this.value, "hello **world**");
assert.strictEqual(
query(".d-editor-preview").innerHTML.trim(),
"<p>hello <strong>world</strong></p>"
);
});
test("links in preview are not tabbable", async function (assert) {
await render(hbs`<DEditor @value={{this.value}} />`);
await fillIn(".d-editor-input", "[discourse](https://www.discourse.org)");
assert.strictEqual(
query(".d-editor-preview").innerHTML.trim(),
'<p><a href="https://www.discourse.org" tabindex="-1">discourse</a></p>'
);
});
test("preview sanitizes HTML", async function (assert) {
await render(hbs`<DEditor @value={{this.value}} />`);
await fillIn(".d-editor-input", `"><svg onload="prompt(/xss/)"></svg>`);
assert.strictEqual(
query(".d-editor-preview").innerHTML.trim(),
'<p>"&gt;</p>'
);
});
test("updating the value refreshes the preview", async function (assert) {
this.set("value", "evil trout");
await render(hbs`<DEditor @value={{this.value}} />`);
assert.strictEqual(
query(".d-editor-preview").innerHTML.trim(),
"<p>evil trout</p>"
);
this.set("value", "zogstrip");
await settled();
assert.strictEqual(
query(".d-editor-preview").innerHTML.trim(),
"<p>zogstrip</p>"
);
});
function jumpEnd(textarea) {
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.value.length;
return textarea;
}
function testCase(title, testFunc) {
chromeTest(title, async function (assert) {
this.set("value", "hello world.");
await render(hbs`<DEditor @value={{this.value}} />`);
const textarea = jumpEnd(query("textarea.d-editor-input"));
await testFunc.call(this, assert, textarea);
});
}
function composerTestCase(title, testFunc) {
test(title, async function (assert) {
this.set("value", "hello world.");
await render(
hbs`<DEditor @value={{this.value}} @composerEvents={{true}} />`
);
const textarea = jumpEnd(query("textarea.d-editor-input"));
await testFunc.call(this, assert, textarea);
});
}
testCase(
`selecting the space before a word`,
async function (assert, textarea) {
textarea.selectionStart = 5;
textarea.selectionEnd = 7;
await click(`button.bold`);
assert.strictEqual(this.value, `hello **w**orld.`);
assert.strictEqual(textarea.selectionStart, 8);
assert.strictEqual(textarea.selectionEnd, 9);
}
);
testCase(
`selecting the space after a word`,
async function (assert, textarea) {
textarea.selectionStart = 0;
textarea.selectionEnd = 6;
await click(`button.bold`);
assert.strictEqual(this.value, `**hello** world.`);
assert.strictEqual(textarea.selectionStart, 2);
assert.strictEqual(textarea.selectionEnd, 7);
}
);
testCase(`bold button with no selection`, async function (assert, textarea) {
await click(`button.bold`);
const example = I18n.t(`composer.bold_text`);
assert.strictEqual(this.value, `hello world.**${example}**`);
assert.strictEqual(textarea.selectionStart, 14);
assert.strictEqual(textarea.selectionEnd, 14 + example.length);
});
testCase(`bold button with a selection`, async function (assert, textarea) {
textarea.selectionStart = 6;
textarea.selectionEnd = 11;
await click(`button.bold`);
assert.strictEqual(this.value, `hello **world**.`);
assert.strictEqual(textarea.selectionStart, 8);
assert.strictEqual(textarea.selectionEnd, 13);
await click(`button.bold`);
assert.strictEqual(this.value, "hello world.");
assert.strictEqual(textarea.selectionStart, 6);
assert.strictEqual(textarea.selectionEnd, 11);
});
testCase(
"bold button maintains undo history",
async function (assert, textarea) {
textarea.selectionStart = 6;
textarea.selectionEnd = 11;
await click("button.bold");
assert.strictEqual(this.value, "hello **world**.");
assert.strictEqual(textarea.selectionStart, 8);
assert.strictEqual(textarea.selectionEnd, 13);
document.execCommand("undo");
assert.strictEqual(this.value, "hello world.");
}
);
testCase(
`bold with a multiline selection`,
async function (assert, textarea) {
this.set("value", "hello\n\nworld\n\ntest.");
textarea.selectionStart = 0;
textarea.selectionEnd = 12;
await click(`button.bold`);
assert.strictEqual(this.value, `**hello**\n\n**world**\n\ntest.`);
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 20);
await click(`button.bold`);
assert.strictEqual(this.value, `hello\n\nworld\n\ntest.`);
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 12);
}
);
testCase(
`italic button with no selection`,
async function (assert, textarea) {
await click(`button.italic`);
const example = I18n.t(`composer.italic_text`);
assert.strictEqual(this.value, `hello world.*${example}*`);
assert.strictEqual(textarea.selectionStart, 13);
assert.strictEqual(textarea.selectionEnd, 13 + example.length);
}
);
testCase(`italic button with a selection`, async function (assert, textarea) {
textarea.selectionStart = 6;
textarea.selectionEnd = 11;
await click(`button.italic`);
assert.strictEqual(this.value, `hello *world*.`);
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 12);
await click(`button.italic`);
assert.strictEqual(this.value, "hello world.");
assert.strictEqual(textarea.selectionStart, 6);
assert.strictEqual(textarea.selectionEnd, 11);
});
testCase(
`italic with a multiline selection`,
async function (assert, textarea) {
this.set("value", "hello\n\nworld\n\ntest.");
textarea.selectionStart = 0;
textarea.selectionEnd = 12;
await click(`button.italic`);
assert.strictEqual(this.value, `*hello*\n\n*world*\n\ntest.`);
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 16);
await click(`button.italic`);
assert.strictEqual(this.value, `hello\n\nworld\n\ntest.`);
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 12);
}
);
test("advanced code", async function (assert) {
this.siteSettings.code_formatting_style = "4-spaces-indent";
this.set(
"value",
`
function xyz(x, y, z) {
if (y === z) {
return true;
}
}
`
);
await render(hbs`<DEditor @value={{this.value}} />`);
const textarea = query("textarea.d-editor-input");
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
await click("button.code");
assert.strictEqual(
this.value,
`
function xyz(x, y, z) {
if (y === z) {
return true;
}
}
`
);
});
test("code button", async function (assert) {
this.siteSettings.code_formatting_style = "4-spaces-indent";
await render(hbs`<DEditor @value={{this.value}} />`);
const textarea = jumpEnd(query("textarea.d-editor-input"));
await click("button.code");
assert.strictEqual(this.value, ` ${I18n.t("composer.code_text")}`);
this.set("value", "first line\n\nsecond line\n\nthird line");
textarea.selectionStart = 11;
textarea.selectionEnd = 11;
await click("button.code");
assert.strictEqual(
this.value,
`first line
${I18n.t("composer.code_text")}
second line
third line`
);
this.set("value", "first line\n\nsecond line\n\nthird line");
await click("button.code");
assert.strictEqual(
this.value,
`first line
second line
third line\`${I18n.t("composer.code_title")}\``
);
this.set("value", "first line\n\nsecond line\n\nthird line");
textarea.selectionStart = 5;
textarea.selectionEnd = 5;
await click("button.code");
assert.strictEqual(
this.value,
`first\`${I18n.t("composer.code_title")}\` line
second line
third line`
);
this.set("value", "first line\n\nsecond line\n\nthird line");
textarea.selectionStart = 6;
textarea.selectionEnd = 10;
await click("button.code");
assert.strictEqual(this.value, "first `line`\n\nsecond line\n\nthird line");
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 11);
await click("button.code");
assert.strictEqual(this.value, "first line\n\nsecond line\n\nthird line");
assert.strictEqual(textarea.selectionStart, 6);
assert.strictEqual(textarea.selectionEnd, 10);
textarea.selectionStart = 0;
textarea.selectionEnd = 23;
await click("button.code");
assert.strictEqual(
this.value,
" first line\n\n second line\n\nthird line"
);
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 31);
await click("button.code");
assert.strictEqual(this.value, "first line\n\nsecond line\n\nthird line");
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 23);
});
test("code fences", async function (assert) {
this.set("value", "");
await render(hbs`<DEditor @value={{this.value}} />`);
const textarea = jumpEnd(query("textarea.d-editor-input"));
await click("button.code");
assert.strictEqual(
this.value,
`\`\`\`
${I18n.t("composer.paste_code_text")}
\`\`\``
);
assert.strictEqual(textarea.selectionStart, 4);
assert.strictEqual(textarea.selectionEnd, 27);
this.set("value", "first line\nsecond line\nthird line");
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
await click("button.code");
assert.strictEqual(
this.value,
`\`\`\`
first line
second line
third line
\`\`\`
`
);
assert.strictEqual(textarea.selectionStart, textarea.value.length);
assert.strictEqual(textarea.selectionEnd, textarea.value.length);
this.set("value", "first line\nsecond line\nthird line");
textarea.selectionStart = 0;
textarea.selectionEnd = 0;
await click("button.code");
assert.strictEqual(
this.value,
`\`${I18n.t("composer.code_title")}\`first line
second line
third line`
);
assert.strictEqual(textarea.selectionStart, 1);
assert.strictEqual(
textarea.selectionEnd,
I18n.t("composer.code_title").length + 1
);
this.set("value", "first line\nsecond line\nthird line");
textarea.selectionStart = 0;
textarea.selectionEnd = 10;
await click("button.code");
assert.strictEqual(
this.value,
`\`first line\`
second line
third line`
);
assert.strictEqual(textarea.selectionStart, 1);
assert.strictEqual(textarea.selectionEnd, 11);
this.set("value", "first line\nsecond line\nthird line");
textarea.selectionStart = 0;
textarea.selectionEnd = 23;
await click("button.code");
assert.strictEqual(
this.value,
`\`\`\`
first line
second line
\`\`\`
third line`
);
assert.strictEqual(textarea.selectionStart, 30);
assert.strictEqual(textarea.selectionEnd, 30);
this.set("value", "first line\nsecond line\nthird line");
textarea.selectionStart = 6;
textarea.selectionEnd = 17;
await click("button.code");
assert.strictEqual(
this.value,
`first \n\`\`\`\nline\nsecond\n\`\`\`\n line\nthird line`
);
assert.strictEqual(textarea.selectionStart, 27);
assert.strictEqual(textarea.selectionEnd, 27);
document.execCommand("undo");
assert.strictEqual(this.value, "first line\nsecond line\nthird line");
});
test("quote button - empty lines", async function (assert) {
this.set("value", "one\n\ntwo\n\nthree");
await render(
hbs`<DEditor @value={{this.value}} @composerEvents={{true}} />`
);
const textarea = jumpEnd(query("textarea.d-editor-input"));
textarea.selectionStart = 0;
await click("button.blockquote");
assert.strictEqual(this.value, "> one\n> \n> two\n> \n> three");
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 25);
await click("button.blockquote");
assert.strictEqual(this.value, "one\n\ntwo\n\nthree");
});
test("quote button - selecting empty lines", async function (assert) {
this.set("value", "one\n\n\n\ntwo");
await render(
hbs`<DEditor @value={{this.value}} @composerEvents={{true}} />`
);
const textarea = jumpEnd(query("textarea.d-editor-input"));
textarea.selectionStart = 6;
textarea.selectionEnd = 10;
await click("button.blockquote");
assert.strictEqual(this.value, "one\n\n\n> \n> two");
document.execCommand("undo");
assert.strictEqual(this.value, "one\n\n\n\ntwo");
});
testCase("quote button", async function (assert, textarea) {
textarea.selectionStart = 6;
textarea.selectionEnd = 9;
await click("button.blockquote");
assert.strictEqual(this.value, "hello\n\n> wor\n\nld.");
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 12);
await click("button.blockquote");
assert.strictEqual(this.value, "hello\n\nwor\n\nld.");
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 10);
textarea.selectionStart = 15;
textarea.selectionEnd = 15;
await click("button.blockquote");
assert.strictEqual(this.value, "hello\n\nwor\n\nld.\n\n> Blockquote");
});
testCase(
`bullet button with no selection`,
async function (assert, textarea) {
const example = I18n.t("composer.list_item");
await click(`button.bullet`);
assert.strictEqual(this.value, `hello world.\n\n* ${example}`);
assert.strictEqual(textarea.selectionStart, 14);
assert.strictEqual(textarea.selectionEnd, 16 + example.length);
await click(`button.bullet`);
assert.strictEqual(this.value, `hello world.\n\n${example}`);
}
);
testCase(`bullet button with a selection`, async function (assert, textarea) {
textarea.selectionStart = 6;
textarea.selectionEnd = 11;
await click(`button.bullet`);
assert.strictEqual(this.value, `hello\n\n* world\n\n.`);
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 14);
await click(`button.bullet`);
assert.strictEqual(this.value, `hello\n\nworld\n\n.`);
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 12);
});
testCase(
`bullet button with a multiple line selection`,
async function (assert, textarea) {
this.set("value", "* Hello\n\nWorld\n\nEvil");
textarea.selectionStart = 0;
textarea.selectionEnd = 20;
await click(`button.bullet`);
assert.strictEqual(this.value, "Hello\n\nWorld\n\nEvil");
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 18);
await click(`button.bullet`);
assert.strictEqual(this.value, "* Hello\n\n* World\n\n* Evil");
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 24);
}
);
testCase(`list button with no selection`, async function (assert, textarea) {
const example = I18n.t("composer.list_item");
await click(`button.list`);
assert.strictEqual(this.value, `hello world.\n\n1. ${example}`);
assert.strictEqual(textarea.selectionStart, 14);
assert.strictEqual(textarea.selectionEnd, 17 + example.length);
await click(`button.list`);
assert.strictEqual(this.value, `hello world.\n\n${example}`);
assert.strictEqual(textarea.selectionStart, 14);
assert.strictEqual(textarea.selectionEnd, 14 + example.length);
});
testCase(`list button with a selection`, async function (assert, textarea) {
textarea.selectionStart = 6;
textarea.selectionEnd = 11;
await click(`button.list`);
assert.strictEqual(this.value, `hello\n\n1. world\n\n.`);
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 15);
await click(`button.list`);
assert.strictEqual(this.value, `hello\n\nworld\n\n.`);
assert.strictEqual(textarea.selectionStart, 7);
assert.strictEqual(textarea.selectionEnd, 12);
});
testCase(`list button with line sequence`, async function (assert, textarea) {
this.set("value", "Hello\n\nWorld\n\nEvil");
textarea.selectionStart = 0;
textarea.selectionEnd = 18;
await click(`button.list`);
assert.strictEqual(this.value, "1. Hello\n\n2. World\n\n3. Evil");
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 27);
await click(`button.list`);
assert.strictEqual(this.value, "Hello\n\nWorld\n\nEvil");
assert.strictEqual(textarea.selectionStart, 0);
assert.strictEqual(textarea.selectionEnd, 18);
});
test("clicking the toggle-direction changes dir from ltr to rtl", async function (assert) {
this.siteSettings.support_mixed_text_direction = true;
this.siteSettings.default_locale = "en";
await render(hbs`<DEditor @value={{this.value}} />`);
await click("button.toggle-direction");
assert.strictEqual(
query("textarea.d-editor-input").getAttribute("dir"),
"rtl"
);
});
test("clicking the toggle-direction changes dir from ltr to rtl", async function (assert) {
this.siteSettings.support_mixed_text_direction = true;
this.siteSettings.default_locale = "en";
await render(hbs`<DEditor @value={{this.value}} />`);
const textarea = query("textarea.d-editor-input");
textarea.setAttribute("dir", "ltr");
await click("button.toggle-direction");
assert.strictEqual(textarea.getAttribute("dir"), "rtl");
});
testCase(
`doesn't jump to bottom with long text`,
async function (assert, textarea) {
this.set("value", "hello world.".repeat(8));
textarea.scrollTop = 0;
textarea.selectionStart = 3;
textarea.selectionEnd = 3;
await click("button.bold");
assert.strictEqual(textarea.scrollTop, 0, "it stays scrolled up");
}
);
test("emoji", async function (assert) {
// Test adding a custom button
withPluginApi("0.1", (api) => {
api.onToolbarCreate((toolbar) => {
toolbar.addButton({
id: "emoji",
group: "extras",
icon: "far-smile",
action: () => toolbar.context.send("emoji"),
});
});
});
this.set("value", "hello world.");
await render(hbs`<DEditor @value={{this.value}} />`);
jumpEnd(query("textarea.d-editor-input"));
await click("button.emoji");
await click(
'.emoji-picker .section[data-section="smileys_&_emotion"] img.emoji[title="grinning"]'
);
assert.strictEqual(
this.value,
"hello world. :grinning:",
"it works when there is no partial emoji"
);
await click("textarea.d-editor-input");
await fillIn(".d-editor-input", "starting to type an emoji like :gri");
jumpEnd(query("textarea.d-editor-input"));
await click("button.emoji");
await click(
'.emoji-picker .section[data-section="smileys_&_emotion"] img.emoji[title="grinning"]'
);
assert.strictEqual(
this.value,
"starting to type an emoji like :grinning:",
"it works when there is a partial emoji"
);
});
test("toolbar buttons tabindex", async function (assert) {
await render(hbs`<DEditor />`);
const buttons = queryAll(".d-editor-button-bar .btn");
assert.strictEqual(
buttons[0].getAttribute("tabindex"),
"0",
"it makes the first button focusable"
);
assert.strictEqual(buttons[1].getAttribute("tabindex"), "-1");
});
testCase("replace-text event by default", async function (assert) {
this.set("value", "red green blue");
await this.container
.lookup("service:app-events")
.trigger("composer:replace-text", "green", "yellow");
assert.strictEqual(this.value, "red green blue");
});
composerTestCase("replace-text event for composer", async function (assert) {
this.set("value", "red green blue");
await this.container
.lookup("service:app-events")
.trigger("composer:replace-text", "green", "yellow");
assert.strictEqual(this.value, "red yellow blue");
});
async function indentSelection(container, direction) {
await container
.lookup("service:app-events")
.trigger("composer:indent-selected-text", direction);
}
composerTestCase(
"indents a single line of text to the right",
async function (assert, textarea) {
this.set("value", "Hello world");
setTextareaSelection(textarea, 0, textarea.value.length);
await indentSelection(this.container, "right");
assert.strictEqual(
this.value,
" Hello world",
"a single line of selection is indented correctly"
);
}
);
composerTestCase(
"de-indents a single line of text to the left",
async function (assert, textarea) {
this.set("value", " Hello world");
setTextareaSelection(textarea, 0, textarea.value.length);
await indentSelection(this.container, "left");
assert.strictEqual(
this.value,
"Hello world",
"a single line of selection is deindented correctly"
);
}
);
composerTestCase(
"indents multiple lines of text to the right",
async function (assert, textarea) {
this.set("value", " Hello world\nThis is me");
setTextareaSelection(textarea, 2, textarea.value.length);
await indentSelection(this.container, "right");
assert.strictEqual(
this.value,
" Hello world\n This is me",
"multiple lines are indented correctly without selecting preceding space"
);
this.set("value", " Hello world\nThis is me");
setTextareaSelection(textarea, 0, textarea.value.length);
await indentSelection(this.container, "right");
assert.strictEqual(
this.value,
" Hello world\n This is me",
"multiple lines are indented correctly with selecting preceding space"
);
}
);
composerTestCase(
"de-indents multiple lines of text to the left",
async function (assert, textarea) {
this.set("value", " Hello world\nThis is me");
setTextareaSelection(textarea, 2, textarea.value.length);
await indentSelection(this.container, "left");
assert.strictEqual(
this.value,
"Hello world\nThis is me",
"multiple lines are de-indented correctly without selecting preceding space"
);
}
);
composerTestCase(
"detects the indentation character (tab vs. string) and uses that",
async function (assert, textarea) {
this.set(
"value",
"```\nfunc init() {\n strings = generateStrings()\n}\n```"
);
setTextareaSelection(textarea, 4, textarea.value.length - 4);
await indentSelection(this.container, "right");
assert.strictEqual(
this.value,
"```\n func init() {\n strings = generateStrings()\n }\n```",
"detects the prevalent indentation character and uses that (tab)"
);
}
);
test("paste table", async function (assert) {
this.set("value", "");
this.siteSettings.enable_rich_text_paste = true;
await render(
hbs`<DEditor @value={{this.value}} @composerEvents={{true}} />`
);
let element = query(".d-editor");
await paste(element, "\ta\tb\n1\t2\t3");
assert.strictEqual(this.value, "||a|b|\n|---|---|---|\n|1|2|3|\n");
document.execCommand("undo");
assert.strictEqual(this.value, "");
});
test("paste a different table", async function (assert) {
this.set("value", "");
this.siteSettings.enable_rich_text_paste = true;
await render(
hbs`<DEditor @value={{this.value}} @composerEvents={{true}} />`
);
let element = query(".d-editor");
await paste(element, '\ta\tb\n1\t"2\n2.5"\t3');
assert.strictEqual(this.value, "||a|b|\n|---|---|---|\n|1|2<br>2.5|3|\n");
});
testCase(
`pasting a link into a selection applies a link format`,
async function (assert, textarea) {
this.set("value", "See discourse in action");
setTextareaSelection(textarea, 4, 13);
const element = query(".d-editor");
const event = await paste(element, "https://www.discourse.org/");
assert.strictEqual(
this.value,
"See [discourse](https://www.discourse.org/) in action"
);
assert.strictEqual(event.defaultPrevented, true);
document.execCommand("undo");
assert.strictEqual(this.value, "See discourse in action");
}
);
testCase(
`pasting other text into a selection will replace text value`,
async function (assert, textarea) {
this.set("value", "good morning");
setTextareaSelection(textarea, 5, 12);
const element = query(".d-editor");
const event = await paste(element, "evening");
// Synthetic paste events do not manipulate document content.
assert.strictEqual(this.value, "good morning");
assert.strictEqual(event.defaultPrevented, false);
}
);
testCase(
`pasting a url without a selection will insert the url`,
async function (assert, textarea) {
this.set("value", "a link example:");
jumpEnd(textarea);
const element = query(".d-editor");
const event = await paste(element, "https://www.discourse.org/");
// Synthetic paste events do not manipulate document content.
assert.strictEqual(this.value, "a link example:");
assert.strictEqual(event.defaultPrevented, false);
}
);
testCase(
`pasting text that contains urls and other content will use default paste behavior`,
async function (assert, textarea) {
this.set("value", "a link example:");
setTextareaSelection(textarea, 0, 1);
const element = query(".d-editor");
const event = await paste(
element,
"Try out Discourse at: https://www.discourse.org/"
);
// Synthetic paste events do not manipulate document content.
assert.strictEqual(this.value, "a link example:");
assert.strictEqual(event.defaultPrevented, false);
}
);
testCase(
`pasting an email into a selection applies a link format`,
async function (assert, textarea) {
this.set("value", "team email");
setTextareaSelection(textarea, 5, 10);
const element = query(".d-editor");
const event = await paste(element, "mailto:team@discourse.org");
assert.strictEqual(this.value, "team [email](mailto:team@discourse.org)");
assert.strictEqual(event.defaultPrevented, true);
}
);
testCase(
`pasting a url onto a selection that contains urls and other content will use default paste behavior`,
async function (assert, textarea) {
this.set("value", "Try https://www.discourse.org");
setTextareaSelection(textarea, 0, 29);
const element = query(".d-editor");
const event = await paste(element, "https://www.discourse.com/");
// Synthetic paste events do not manipulate document content.
assert.strictEqual(this.value, "Try https://www.discourse.org");
assert.strictEqual(event.defaultPrevented, false);
}
);
testCase(
`pasting a url onto a selection that contains bbcode-like tags will use default paste behavior`,
async function (assert, textarea) {
this.set("value", "hello [url=foobar]foobar[/url]");
setTextareaSelection(textarea, 0, 30);
const element = query(".d-editor");
const event = await paste(element, "https://www.discourse.com/");
// Synthetic paste events do not manipulate document content.
assert.strictEqual(this.value, "hello [url=foobar]foobar[/url]");
assert.strictEqual(event.defaultPrevented, false);
}
);
(() => {
// Tests to check cursor/selection after replace-text event.
const BEFORE = "red green blue";
const NEEDLE = "green";
const REPLACE = "yellow";
const AFTER = BEFORE.replace(NEEDLE, REPLACE);
const CASES = [
{
description: "cursor at start remains there",
before: [0, 0],
after: [0, 0],
},
{
description: "cursor before needle becomes cursor before replacement",
before: [BEFORE.indexOf(NEEDLE), 0],
after: [AFTER.indexOf(REPLACE), 0],
},
{
description: "cursor at needle start + 1 moves behind replacement",
before: [BEFORE.indexOf(NEEDLE) + 1, 0],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0],
},
{
description: "cursor at needle end - 1 stays behind replacement",
before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 0],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0],
},
{
description: "cursor behind needle becomes cursor behind replacement",
before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length, 0],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0],
},
{
description: "cursor at end remains there",
before: [BEFORE.length, 0],
after: [AFTER.length, 0],
},
{
description:
"selection spanning needle start becomes selection until replacement start",
before: [BEFORE.indexOf(NEEDLE) - 1, 2],
after: [AFTER.indexOf(REPLACE) - 1, 1],
},
{
description:
"selection spanning needle end becomes selection from replacement end",
before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 2],
after: [AFTER.indexOf(REPLACE) + REPLACE.length, 1],
},
{
description:
"selection spanning needle becomes selection spanning replacement",
before: [BEFORE.indexOf(NEEDLE) - 1, NEEDLE.length + 2],
after: [AFTER.indexOf(REPLACE) - 1, REPLACE.length + 2],
},
{
description: "complete selection remains complete",
before: [0, BEFORE.length],
after: [0, AFTER.length],
},
];
for (let i = 0; i < CASES.length; i++) {
const CASE = CASES[i];
// prettier-ignore
composerTestCase(`replace-text event: ${CASE.description}`, async function(
assert,
textarea
) {
this.set("value", BEFORE);
await focus(textarea);
assert.strictEqual(textarea.value, BEFORE);
const [start, len] = CASE.before;
setTextareaSelection(textarea, start, start + len);
this.container
.lookup("service:app-events")
.trigger("composer:replace-text", "green", "yellow", { forceFocus: true });
next(() => {
let expect = formatTextWithSelection(AFTER, CASE.after);
let actual = formatTextWithSelection(
this.value,
getTextareaSelection(textarea)
);
assert.strictEqual(actual, expect);
});
});
}
})();
});