import componentTest from 'helpers/component-test'; import { withPluginApi } from 'discourse/lib/plugin-api'; moduleForComponent('d-editor', {integration: true}); componentTest('preview updates with markdown', { template: '{{d-editor value=value}}', test(assert) { assert.ok(this.$('.d-editor-button-bar').length); fillIn('.d-editor-input', 'hello **world**'); andThen(() => { assert.equal(this.get('value'), 'hello **world**'); assert.equal(this.$('.d-editor-preview').html().trim(), '

hello world

'); }); } }); componentTest('preview sanitizes HTML', { template: '{{d-editor value=value}}', test(assert) { this.set('value', `">`); andThen(() => { assert.equal(this.$('.d-editor-preview').html().trim(), '

\">

'); }); } }); componentTest('updating the value refreshes the preview', { template: '{{d-editor value=value}}', setup() { this.set('value', 'evil trout'); }, test(assert) { assert.equal(this.$('.d-editor-preview').html().trim(), '

evil trout

'); andThen(() => this.set('value', 'zogstrip')); andThen(() => assert.equal(this.$('.d-editor-preview').html().trim(), '

zogstrip

')); } }); function jumpEnd(textarea) { textarea.selectionStart = textarea.value.length; textarea.selectionEnd = textarea.value.length; return textarea; } function testCase(title, testFunc) { componentTest(title, { template: '{{d-editor value=value}}', setup() { this.set('value', 'hello world.'); }, test(assert) { const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); testFunc.call(this, assert, textarea); } }); } function composerTestCase(title, testFunc) { componentTest(title, { template: '{{d-editor value=value composerEvents=true}}', setup() { this.set('value', 'hello world.'); }, test(assert) { const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); testFunc.call(this, assert, textarea); } }); } testCase(`selecting the space before a word`, function(assert, textarea) { textarea.selectionStart = 5; textarea.selectionEnd = 7; click(`button.bold`); andThen(() => { assert.equal(this.get('value'), `hello **w**orld.`); assert.equal(textarea.selectionStart, 8); assert.equal(textarea.selectionEnd, 9); }); }); testCase(`selecting the space after a word`, function(assert, textarea) { textarea.selectionStart = 0; textarea.selectionEnd = 6; click(`button.bold`); andThen(() => { assert.equal(this.get('value'), `**hello** world.`); assert.equal(textarea.selectionStart, 2); assert.equal(textarea.selectionEnd, 7); }); }); testCase(`bold button with no selection`, function(assert, textarea) { click(`button.bold`); andThen(() => { const example = I18n.t(`composer.bold_text`); assert.equal(this.get('value'), `hello world.**${example}**`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 14 + example.length); }); }); testCase(`bold button with a selection`, function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; click(`button.bold`); andThen(() => { assert.equal(this.get('value'), `hello **world**.`); assert.equal(textarea.selectionStart, 8); assert.equal(textarea.selectionEnd, 13); }); click(`button.bold`); andThen(() => { assert.equal(this.get('value'), 'hello world.'); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 11); }); }); testCase(`bold with a multiline selection`, function (assert, textarea) { this.set('value', "hello\n\nworld\n\ntest."); andThen(() => { textarea.selectionStart = 0; textarea.selectionEnd = 12; }); click(`button.bold`); andThen(() => { assert.equal(this.get('value'), `**hello**\n\n**world**\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 20); }); click(`button.bold`); andThen(() => { assert.equal(this.get('value'), `hello\n\nworld\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 12); }); }); testCase(`italic button with no selection`, function(assert, textarea) { click(`button.italic`); andThen(() => { const example = I18n.t(`composer.italic_text`); assert.equal(this.get('value'), `hello world._${example}_`); assert.equal(textarea.selectionStart, 13); assert.equal(textarea.selectionEnd, 13 + example.length); }); }); testCase(`italic button with a selection`, function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; click(`button.italic`); andThen(() => { assert.equal(this.get('value'), `hello _world_.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 12); }); click(`button.italic`); andThen(() => { assert.equal(this.get('value'), 'hello world.'); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 11); }); }); testCase(`italic with a multiline selection`, function (assert, textarea) { this.set('value', "hello\n\nworld\n\ntest."); andThen(() => { textarea.selectionStart = 0; textarea.selectionEnd = 12; }); click(`button.italic`); andThen(() => { assert.equal(this.get('value'), `_hello_\n\n_world_\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 16); }); click(`button.italic`); andThen(() => { assert.equal(this.get('value'), `hello\n\nworld\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 12); }); }); testCase('link modal (cancel)', function(assert) { assert.equal(this.$('.insert-link.hidden').length, 1); click('button.link'); andThen(() => { assert.equal(this.$('.insert-link.hidden').length, 0); }); click('.insert-link button.btn-danger'); andThen(() => { assert.equal(this.$('.insert-link.hidden').length, 1); assert.equal(this.get('value'), 'hello world.'); }); }); testCase('link modal (simple link)', function(assert, textarea) { click('button.link'); const url = 'http://eviltrout.com'; fillIn('.insert-link input.link-url', url); click('.insert-link button.btn-primary'); andThen(() => { assert.equal(this.$('.insert-link.hidden').length, 1); assert.equal(this.get('value'), `hello world.[${url}](${url})`); assert.equal(textarea.selectionStart, 13); assert.equal(textarea.selectionEnd, 13 + url.length); }); }); testCase('link modal auto http addition', function(assert) { click('button.link'); fillIn('.insert-link input.link-url', 'sam.com'); click('.insert-link button.btn-primary'); andThen(() => { assert.equal(this.get('value'), `hello world.[sam.com](http://sam.com)`); }); }); testCase('link modal (simple link) with selected text', function(assert, textarea) { textarea.selectionStart = 0; textarea.selectionEnd = 12; click('button.link'); fillIn('.insert-link input.link-url', 'http://eviltrout.com'); click('.insert-link button.btn-primary'); andThen(() => { assert.equal(this.$('.insert-link.hidden').length, 1); assert.equal(this.get('value'), '[hello world.](http://eviltrout.com)'); }); }); testCase('link modal (link with description)', function(assert) { click('button.link'); fillIn('.insert-link input.link-url', 'http://eviltrout.com'); fillIn('.insert-link input.link-text', 'evil trout'); click('.insert-link button.btn-primary'); andThen(() => { assert.equal(this.$('.insert-link.hidden').length, 1); assert.equal(this.get('value'), 'hello world.[evil trout](http://eviltrout.com)'); }); }); componentTest('advanced code', { template: '{{d-editor value=value}}', setup() { this.siteSettings.code_formatting_style = '4-spaces-indent'; this.set('value', ` function xyz(x, y, z) { if (y === z) { return true; } } ` ); }, test(assert) { const textarea = this.$('textarea.d-editor-input')[0]; textarea.selectionStart = 0; textarea.selectionEnd = textarea.value.length; click('button.code'); andThen(() => { assert.equal(this.get('value'), ` function xyz(x, y, z) { if (y === z) { return true; } } ` ); }); } }); componentTest('code button', { template: '{{d-editor value=value}}', setup() { this.siteSettings.code_formatting_style = '4-spaces-indent'; }, test(assert) { const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); click('button.code'); andThen(() => { assert.equal(this.get('value'), ` ${I18n.t('composer.code_text')}` ); this.set('value', "first line\n\nsecond line\n\nthird line"); textarea.selectionStart = 11; textarea.selectionEnd = 11; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `first line ${I18n.t('composer.code_text')} second line third line` ); this.set('value', "first line\n\nsecond line\n\nthird line"); }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `first line second line third line\`${I18n.t('composer.code_title')}\`` ); this.set('value', "first line\n\nsecond line\n\nthird line"); }); andThen(() => { textarea.selectionStart = 5; textarea.selectionEnd = 5; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `first\`${I18n.t('composer.code_title')}\` line second line third line` ); this.set('value', "first line\n\nsecond line\n\nthird line"); }); andThen(() => { textarea.selectionStart = 6; textarea.selectionEnd = 10; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), "first `line`\n\nsecond line\n\nthird line"); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 11); }); click('button.code'); andThen(() => { assert.equal(this.get('value'), "first line\n\nsecond line\n\nthird line"); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 10); textarea.selectionStart = 0; textarea.selectionEnd = 23; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), " first line\n\n second line\n\nthird line"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 31); }); click('button.code'); andThen(() => { assert.equal(this.get('value'), "first line\n\nsecond line\n\nthird line"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 23); }); } }); componentTest('code fences', { template: '{{d-editor value=value}}', setup() { this.set('value', ''); }, test(assert) { const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); click('button.code'); andThen(() => { assert.equal(this.get('value'), `\`\`\` ${I18n.t("composer.paste_code_text")} \`\`\`` ); assert.equal(textarea.selectionStart, 4); assert.equal(textarea.selectionEnd, 27); this.set('value', 'first line\nsecond line\nthird line'); textarea.selectionStart = 0; textarea.selectionEnd = textarea.value.length; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `\`\`\` first line second line third line \`\`\` ` ); assert.equal(textarea.selectionStart, textarea.value.length); assert.equal(textarea.selectionEnd, textarea.value.length); this.set('value', 'first line\nsecond line\nthird line'); textarea.selectionStart = 0; textarea.selectionEnd = 0; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `\`${I18n.t('composer.code_title')}\`first line second line third line` ); assert.equal(textarea.selectionStart, 1); assert.equal(textarea.selectionEnd, I18n.t('composer.code_title').length + 1); this.set('value', 'first line\nsecond line\nthird line'); textarea.selectionStart = 0; textarea.selectionEnd = 10; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `\`first line\` second line third line` ); assert.equal(textarea.selectionStart, 1); assert.equal(textarea.selectionEnd, 11); this.set('value', 'first line\nsecond line\nthird line'); textarea.selectionStart = 0; textarea.selectionEnd = 23; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `\`\`\` first line second line \`\`\` third line` ); assert.equal(textarea.selectionStart, 30); assert.equal(textarea.selectionEnd, 30); this.set('value', 'first line\nsecond line\nthird line'); textarea.selectionStart = 6; textarea.selectionEnd = 17; }); click('button.code'); andThen(() => { assert.equal(this.get('value'), `first \n\`\`\`\nline\nsecond\n\`\`\`\n line\nthird line`); assert.equal(textarea.selectionStart, 27); assert.equal(textarea.selectionEnd, 27); }); } }); testCase('quote button', function(assert, textarea) { click('button.quote'); andThen(() => { assert.equal(this.get('value'), 'hello world.'); }); andThen(() => { textarea.selectionStart = 6; textarea.selectionEnd = 11; }); click('button.quote'); andThen(() => { assert.equal(this.get('value'), 'hello > world.'); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 13); }); click('button.quote'); andThen(() => { assert.equal(this.get('value'), 'hello world.'); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 11); }); }); testCase(`bullet button with no selection`, function(assert, textarea) { const example = I18n.t('composer.list_item'); click(`button.bullet`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n* ${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 16 + example.length); }); click(`button.bullet`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n${example}`); }); }); testCase(`bullet button with a selection`, function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; click(`button.bullet`); andThen(() => { assert.equal(this.get('value'), `hello\n\n* world\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 14); }); click(`button.bullet`); andThen(() => { assert.equal(this.get('value'), `hello\n\nworld\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 12); }); }); testCase(`bullet button with a multiple line selection`, function(assert, textarea) { this.set('value', "* Hello\n\nWorld\n\nEvil"); andThen(() => { textarea.selectionStart = 0; textarea.selectionEnd = 20; }); click(`button.bullet`); andThen(() => { assert.equal(this.get('value'), "Hello\n\nWorld\n\nEvil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 18); }); click(`button.bullet`); andThen(() => { assert.equal(this.get('value'), "* Hello\n\n* World\n\n* Evil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 24); }); }); testCase(`list button with no selection`, function(assert, textarea) { const example = I18n.t('composer.list_item'); click(`button.list`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n1. ${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 17 + example.length); }); click(`button.list`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 14 + example.length); }); }); testCase(`list button with a selection`, function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; click(`button.list`); andThen(() => { assert.equal(this.get('value'), `hello\n\n1. world\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 15); }); click(`button.list`); andThen(() => { assert.equal(this.get('value'), `hello\n\nworld\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 12); }); }); testCase(`list button with line sequence`, function(assert, textarea) { this.set('value', "Hello\n\nWorld\n\nEvil"); andThen(() => { textarea.selectionStart = 0; textarea.selectionEnd = 18; }); click(`button.list`); andThen(() => { assert.equal(this.get('value'), "1. Hello\n\n2. World\n\n3. Evil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 27); }); click(`button.list`); andThen(() => { assert.equal(this.get('value'), "Hello\n\nWorld\n\nEvil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 18); }); }); testCase(`heading button with no selection`, function(assert, textarea) { const example = I18n.t('composer.heading_text'); click(`button.heading`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n## ${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 17 + example.length); }); textarea.selectionStart = 30; textarea.selectionEnd = 30; click(`button.heading`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 14 + example.length); }); }); testCase(`rule between things`, function(assert, textarea) { textarea.selectionStart = 5; textarea.selectionEnd = 5; click(`button.rule`); andThen(() => { assert.equal(this.get('value'), `hello\n\n----------\n world.`); assert.equal(textarea.selectionStart, 18); assert.equal(textarea.selectionEnd, 18); }); }); testCase(`rule with no selection`, function(assert, textarea) { click(`button.rule`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n----------\n`); assert.equal(textarea.selectionStart, 25); assert.equal(textarea.selectionEnd, 25); }); click(`button.rule`); andThen(() => { assert.equal(this.get('value'), `hello world.\n\n----------\n\n\n----------\n`); assert.equal(textarea.selectionStart, 38); assert.equal(textarea.selectionEnd, 38); }); }); testCase(`rule with a selection`, function(assert, textarea) { textarea.selectionStart = 6; textarea.selectionEnd = 11; click(`button.rule`); andThen(() => { assert.equal(this.get('value'), `hello \n\n----------\n.`); assert.equal(textarea.selectionStart, 19); assert.equal(textarea.selectionEnd, 19); }); }); testCase(`doesn't jump to bottom with long text`, function(assert, textarea) { let longText = 'hello world.'; for (let i=0; i<8; i++) { longText = longText + longText; } this.set('value', longText); andThen(() => { $(textarea).scrollTop(0); textarea.selectionStart = 3; textarea.selectionEnd = 3; }); click('button.bold'); andThen(() => { assert.equal($(textarea).scrollTop(), 0, 'it stays scrolled up'); }); }); componentTest('emoji', { template: '{{d-editor value=value}}', setup() { // Test adding a custom button withPluginApi('0.1', api => { api.onToolbarCreate(toolbar => { toolbar.addButton({ id: 'emoji', group: 'extras', icon: 'smile-o', action: 'emoji' }); }); }); this.set('value', 'hello world.'); }, test(assert) { assert.equal($('.emoji-modal').length, 0); jumpEnd(this.$('textarea.d-editor-input')[0]); click('button.emoji'); andThen(() => { assert.equal($('.emoji-modal').length, 1); }); click('a[data-group-id=0]'); click('a[title=grinning]'); andThen(() => { assert.ok($('.emoji-modal').length === 0); assert.equal(this.get('value'), 'hello world.:grinning:'); }); } }); testCase("replace-text event by default", function(assert) { this.set('value', "red green blue"); andThen(() => { this.container.lookup('app-events:main').trigger('composer:replace-text', 'green', 'yellow'); }); andThen(() => { assert.equal(this.get('value'), 'red green blue'); }); }); composerTestCase("replace-text event for composer", function(assert) { this.set('value', "red green blue"); andThen(() => { this.container.lookup('app-events:main').trigger('composer:replace-text', 'green', 'yellow'); }); andThen(() => { assert.equal(this.get('value'), 'red yellow blue'); }); }); (() => { // 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] } ]; function setSelection(textarea, [start, len]) { textarea.selectionStart = start; textarea.selectionEnd = start + len; } function getSelection(textarea) { const start = textarea.selectionStart; const end = textarea.selectionEnd; return [start, end - start]; } function formatTextWithSelection(text, [start, len]) { return [ '"', text.substr(0, start), '<', text.substr(start, len), '>', text.substr(start+len), '"', ].join(''); } for (let i = 0; i < CASES.length; i++) { const CASE = CASES[i]; composerTestCase(`replace-text event: ${CASE.description}`, function(assert, textarea) { this.set('value', BEFORE); setSelection(textarea, CASE.before); andThen(() => { this.container.lookup('app-events:main').trigger('composer:replace-text', 'green', 'yellow'); }); andThen(() => { let expect = formatTextWithSelection(AFTER, CASE.after); let actual = formatTextWithSelection(this.get('value'), getSelection(textarea)); assert.equal(actual, expect); }); }); } })();