feat: Code Widget + Markdown Widget Internal Overhaul (#2828)
* wip - upgrade to slate 0.43 * wip * wip * wip * wip * wip * wip * wip * finish list handling logic * add plugins directory * tests wip * setup testing * wip * add selection commands * finish list testing * stuff * add codemirror * abstract codemirror from slate * wip * wip * wip * wip * wip * wip * wip * wip * wip * codemirror mostly working, some bugs * upgrade to slate 46 * upgrade to slate 47 * wip * wip * progress * wip * mostly working links with surrounding marks * wip * tests passing * add test * fix formatting * update snapshots * close self closing tag in markdown html output * wip - commonmark * hold on commonmark work * all tests passing * fix e2e specs * ignore tests in esm builds * break/backspace plugins wip * finish enter/backspace spec * fix soft break handling * wip - editor component deletion * add insertion points * make insertion points invisible * fix empty mark nodes output to markdown * fix pasting * improve insertion points * add static bottom insertion point * improve click handling at insertion points * restore current table functionality * add paste support for Slate fragments * support cut/copy markdown, paste between rich/raw editor * fix copy paste * wip - paste/select bug fixing * fixed known slate issues * split plugins * fix editor toggles * force text cursor in code widget * wip - reorg plugins * finish markdown control reorg * configure plugin types * quote block adjacent handling with tests * wip * finish quote logic and tests * fix copy paste plugin migration regressions * fix force insert before node * fix trailing insertion point * remove empty headers * codemirror working properly in markdown widget * return focus to codemirror on lang select enter * fix state issues for widgets with local state * wip - vim working, just need to work out distribution * add settings pane * wip - default modes * fix deps * add programming language data * implement linguist langs in code widget * everything built in * remove old registration code, fix focus styling * fix/update linting setup * fix js lint errors * remove stylelint from format script * fix remaining linting errors * fix reducer test failures * chore: update commitlint for worktree support * chore: fix remaining tests * chore: drop unused monaco plugin * chore: remove extraneous global styles rendering * chore: fix failing tests * fix: tests * fix: quote/list nesting (tests still broken) * fix: update quote tests * chore: bring back code widget test config * fix: autofocus * fix: code blocks without the code widget * fix: code editor component state issues * fix: error * fix: add code block test, few fixes * chore: remove notes * fix: [wip] update stateful shortcodes on undo/redo * fix: support code styled links, handle unknown langs * fix: few fixes * fix: autofocus on insert, focus on all clicks * fix: linting * fix: autofocus * fix: update code block fixture * fix: remove unused cypress snapshot plugin * fix: drop node 8 test, add node 12 * fix: use lodash.flatten instead of Array.flat * fix: remove console logs
This commit is contained in:
committed by
Erez Rokah
parent
be46293f82
commit
18c579d0e9
82
cypress/integration/markdown_widget_backspace_spec.js
Normal file
82
cypress/integration/markdown_widget_backspace_spec.js
Normal file
@ -0,0 +1,82 @@
|
||||
import '../utils/dismiss-local-backup';
|
||||
|
||||
describe('Markdown widget', () => {
|
||||
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
cy.loginAndNewPost();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('pressing backspace', () => {
|
||||
it('sets non-default block to default when empty', () => {
|
||||
cy.focused()
|
||||
.clickHeadingOneButton()
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('does nothing at start of first block in document when non-empty and non-default', () => {
|
||||
cy.focused()
|
||||
.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.setCursorBefore('foo')
|
||||
.backspace({ times: 4 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>foo</h1>
|
||||
`);
|
||||
});
|
||||
it('deletes individual characters in middle of non-empty non-default block in document', () => {
|
||||
cy.focused()
|
||||
.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.setCursorAfter('fo')
|
||||
.backspace({ times: 3 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>o</h1>
|
||||
`);
|
||||
});
|
||||
it('at beginning of non-first block, moves default block content to previous block', () => {
|
||||
cy.focused()
|
||||
.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setCursorBefore('bar')
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>foobar</h1>
|
||||
`);
|
||||
});
|
||||
it('at beginning of non-first block, moves non-default block content to previous block', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.clickHeadingOneButton()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.clickHeadingTwoButton()
|
||||
.type('baz')
|
||||
.setCursorBefore('baz')
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<h1>barbaz</h1>
|
||||
`)
|
||||
.setCursorBefore('bar')
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foobarbaz</p>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
120
cypress/integration/markdown_widget_code_block_spec.js
Normal file
120
cypress/integration/markdown_widget_code_block_spec.js
Normal file
@ -0,0 +1,120 @@
|
||||
import { oneLineTrim, stripIndent } from 'common-tags';
|
||||
import '../utils/dismiss-local-backup';
|
||||
|
||||
describe('Markdown widget', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
cy.loginAndNewPost();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
describe('code block', () => {
|
||||
it('outputs code', () => {
|
||||
cy.insertCodeBlock()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.confirmMarkdownEditorContent(`
|
||||
${codeBlock(`
|
||||
foo
|
||||
bar
|
||||
`)}
|
||||
`)
|
||||
.clickModeToggle()
|
||||
.confirmMarkdownEditorContent(`
|
||||
${codeBlockRaw(`
|
||||
foo
|
||||
bar
|
||||
`)}
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function codeBlockRaw(content) {
|
||||
return ['```', ...stripIndent(content).split('\n'), '```'].map(line => oneLineTrim`
|
||||
<div>
|
||||
<span>
|
||||
<span>
|
||||
<span>${line}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function codeBlock(content) {
|
||||
const lines = stripIndent(content).split('\n').map((line, idx) => `
|
||||
<div>
|
||||
<div>
|
||||
<div>${idx + 1}</div>
|
||||
</div>
|
||||
<pre><span>${line}</span></pre>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
return oneLineTrim`
|
||||
<div>
|
||||
<div><span><span><span><span></span><span></span></span></span></span></div>
|
||||
<div>
|
||||
<div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div><label>Code Block</label>
|
||||
<div><button><span><svg>
|
||||
<path></path>
|
||||
</svg></span></button>
|
||||
<div>
|
||||
<div>
|
||||
<div><textarea></textarea></div>
|
||||
<div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<pre><span>xxxxxxxxxx</span></pre>
|
||||
</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div> </div>
|
||||
</div>
|
||||
<div>
|
||||
${lines}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
108
cypress/integration/markdown_widget_enter_spec.js
Normal file
108
cypress/integration/markdown_widget_enter_spec.js
Normal file
@ -0,0 +1,108 @@
|
||||
import '../utils/dismiss-local-backup';
|
||||
|
||||
describe('Markdown widget breaks', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
cy.loginAndNewPost();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('pressing enter', () => {
|
||||
it('creates new default block from empty block', () => {
|
||||
cy.focused()
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('creates new default block when selection collapsed at end of block', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('creates new default block when selection collapsed at end of non-default block', () => {
|
||||
cy.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>foo</h1>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('creates new default block when selection collapsed in empty non-default block', () => {
|
||||
cy.clickHeadingOneButton()
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1></h1>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('splits block into two same-type blocks when collapsed selection at block start', () => {
|
||||
cy.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.setCursorBefore('foo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1></h1>
|
||||
<h1>foo</h1>
|
||||
`);
|
||||
});
|
||||
it('splits block into two same-type blocks when collapsed in middle of selection at block start', () => {
|
||||
cy.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.setCursorBefore('oo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>f</h1>
|
||||
<h1>oo</h1>
|
||||
`);
|
||||
});
|
||||
it('deletes selected content and splits to same-type block when selection is expanded', () => {
|
||||
cy.clickHeadingOneButton()
|
||||
.type('foo bar')
|
||||
.setSelection('o b')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>fo</h1>
|
||||
<h1>ar</h1>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pressing shift+enter', () => {
|
||||
it('creates line break', () => {
|
||||
cy.focused()
|
||||
.enter({ shift: true })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>
|
||||
<br>
|
||||
</p>
|
||||
`);
|
||||
});
|
||||
it('creates consecutive line break', () => {
|
||||
cy.focused()
|
||||
.enter({ shift: true, times: 4 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</p>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
681
cypress/integration/markdown_widget_list_spec.js
Normal file
681
cypress/integration/markdown_widget_list_spec.js
Normal file
@ -0,0 +1,681 @@
|
||||
import '../utils/dismiss-local-backup';
|
||||
|
||||
describe('Markdown widget', () => {
|
||||
describe('list', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
cy.loginAndNewPost();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('toolbar buttons', () => {
|
||||
it('creates and focuses empty list', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('removes list', () => {
|
||||
cy.clickUnorderedListButton({ times: 2 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates nested list when selection is collapsed in non-first block of list item', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.type('bar')
|
||||
.enter()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('converts empty nested list item to empty block in parent list item', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.clickUnorderedListButton()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.backspace({ times: 4 })
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('moves nested list item content to parent list item when in first block', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.clickUnorderedListButton()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.clickUnorderedListButton()
|
||||
.type('baz')
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('affects only the current block with collapsed selection', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.type('baz')
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>baz</p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('combines adjacent same-typed lists, not differently typed lists', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.type('baz')
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>baz</p>
|
||||
`)
|
||||
.down({ times: 2 })
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.up()
|
||||
.enter()
|
||||
.type('qux')
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>qux</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.up()
|
||||
.enter()
|
||||
.type('quux')
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>quux</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>qux</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.clickOrderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>quux</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<p>qux</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.setSelection({
|
||||
anchorQuery: 'ul > li > ol p',
|
||||
anchorOffset: 1,
|
||||
focusQuery: 'ul > li > ul:last-child p',
|
||||
focusOffset: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('affects only selected list items', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.type('bar')
|
||||
.enter({ times: 2 })
|
||||
.type('baz')
|
||||
.setSelection('bar')
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.clickUnorderedListButton()
|
||||
.setSelection('bar', 'baz')
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
`)
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.setSelection('baz')
|
||||
.clickUnorderedListButton()
|
||||
.setCursorAfter('baz')
|
||||
.enter()
|
||||
.clickUnorderedListButton()
|
||||
.type('qux')
|
||||
.setSelection('baz')
|
||||
.clickOrderedListButton()
|
||||
.setCursorAfter('qux')
|
||||
.enter({ times: 4 })
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>qux</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Enter', () => {
|
||||
it('removes the list item and list if empty', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a new paragraph in a non-empty paragraph within a list item', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.type('bar')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a new list item in an empty paragraph within a non-empty list item', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.type('bar')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a new block below list', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 3 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Backspace', () => {
|
||||
it('removes the list item and list if empty', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('removes empty block in non-empty list item', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('removes the list item if list not empty', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not remove list item if empty with non-default block', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.clickHeadingOneButton()
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Tab', () => {
|
||||
it('does nothing in top level list', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.type('foo')
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
});
|
||||
|
||||
it('indents nested list items', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.type('bar')
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.enter({ times: 2 })
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
});
|
||||
|
||||
it('only nests up to one level down from the parent list', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.tabkey({ times: 5 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('unindents nested list items with shift', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.tabkey()
|
||||
.tabkey({ shift: true })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
});
|
||||
|
||||
it('indents and unindents from one level below parent back to document root', () => {
|
||||
cy.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.tabkey()
|
||||
.type('bar')
|
||||
.enter({ times: 2 })
|
||||
.tabkey()
|
||||
.type('baz')
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.tabkey({ shift: true })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.tabkey({ shift: true })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
36
cypress/integration/markdown_widget_marks_spec.js
Normal file
36
cypress/integration/markdown_widget_marks_spec.js
Normal file
@ -0,0 +1,36 @@
|
||||
import '../utils/dismiss-local-backup';
|
||||
|
||||
describe('Markdown widget', () => {
|
||||
describe('code mark', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
cy.loginAndNewPost();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('toolbar button', () => {
|
||||
it('can combine code mark with other marks', () => {
|
||||
cy.clickItalicButton()
|
||||
.type('foo')
|
||||
.setSelection('oo')
|
||||
.clickCodeButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>
|
||||
<em>f</em>
|
||||
<code>
|
||||
<em>oo</em>
|
||||
</code>
|
||||
</p>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
307
cypress/integration/markdown_widget_quote_spec.js
Normal file
307
cypress/integration/markdown_widget_quote_spec.js
Normal file
@ -0,0 +1,307 @@
|
||||
import '../utils/dismiss-local-backup';
|
||||
|
||||
describe('Markdown widget', () => {
|
||||
describe('quote block', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
cy.loginAndNewPost();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('toggle quote', () => {
|
||||
it('toggles empty quote block on and off in empty editor', () => {
|
||||
cy.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p></p>
|
||||
</blockquote>
|
||||
`)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('toggles empty quote block on and off for current block', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
`)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
`);
|
||||
});
|
||||
it('toggles entire quote block without expanded selection', () => {
|
||||
cy.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
`);
|
||||
});
|
||||
it('toggles entire quote block with complex content', () => {
|
||||
cy.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.enter({ times: 3 })
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<h1>foo</h1>
|
||||
</li>
|
||||
</ul>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('toggles empty quote block on and off for selected blocks', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setSelection('foo', 'bar')
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
`)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('toggles empty quote block on and off for partially selected blocks', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setSelection('oo', 'ba')
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
`)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('toggles quote block on and off for multiple selected list items', () => {
|
||||
cy.focused()
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.type('bar')
|
||||
.setSelection('foo', 'bar')
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
`)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
`)
|
||||
.setCursorAfter('bar')
|
||||
.enter({ times: 2 })
|
||||
.type('baz')
|
||||
.setSelection('bar', 'baz')
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
`)
|
||||
});
|
||||
it('creates new quote block if parent is not a quote, can deeply nest', () => {
|
||||
cy.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter({ times: 10 })
|
||||
.type('bar')
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`)
|
||||
.backspace({ times: 12 })
|
||||
});
|
||||
});
|
||||
|
||||
describe('backspace inside quote', () => {
|
||||
it('joins two paragraphs', () => {
|
||||
cy.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setCursorBefore('bar')
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foobar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('joins quote with previous quote', () => {
|
||||
cy.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.clickQuoteButton()
|
||||
.type('bar')
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
<blockquote>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`)
|
||||
.setCursorBefore('bar')
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('removes first block from quote when focused at first block at start', () => {
|
||||
cy.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setCursorBefore('foo')
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<blockquote>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`)
|
||||
});
|
||||
});
|
||||
|
||||
describe('enter inside quote', () => {
|
||||
it('creates new block inside quote', () => {
|
||||
cy.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
</blockquote>
|
||||
`)
|
||||
.type('bar')
|
||||
.setCursorAfter('ba')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>ba</p>
|
||||
<p>r</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('creates new block after quote from empty last block', () => {
|
||||
cy.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
<p></p>
|
||||
`)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -23,8 +23,11 @@
|
||||
//
|
||||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
const { escapeRegExp } = require('../utils/regexp');
|
||||
const path = require('path');
|
||||
import path from 'path';
|
||||
import rehype from 'rehype';
|
||||
import visit from 'unist-util-visit';
|
||||
import { oneLineTrim } from 'common-tags';
|
||||
import { escapeRegExp } from '../utils/regexp';
|
||||
|
||||
const matchRoute = (route, fetchArgs) => {
|
||||
const url = fetchArgs[0];
|
||||
@ -86,3 +89,241 @@ Cypress.Commands.add('stubFetch', ({ fixture }) => {
|
||||
cy.on('window:before:load', win => stubFetch(win, routes));
|
||||
});
|
||||
});
|
||||
|
||||
function runTimes(cyInstance, fn, count = 1) {
|
||||
let chain = cyInstance, i = count;
|
||||
while (i) {
|
||||
i -= 1;
|
||||
chain = fn(chain);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
[
|
||||
'enter',
|
||||
'backspace',
|
||||
['selectAll', 'selectall'],
|
||||
['up', 'upArrow'],
|
||||
['down', 'downArrow'],
|
||||
['left', 'leftArrow'],
|
||||
['right', 'rightArrow'],
|
||||
].forEach(key => {
|
||||
const [ cmd, keyName ] = typeof key === 'object' ? key : [key, key];
|
||||
Cypress.Commands.add(cmd, { prevSubject: true }, (subject, { shift, times = 1 } = {}) => {
|
||||
const fn = chain => chain.type(`${shift ? '{shift}' : ''}{${keyName}}`);
|
||||
return runTimes(cy.wrap(subject), fn, times);
|
||||
});
|
||||
});
|
||||
|
||||
// Convert `tab` command from plugin to a child command with `times` support
|
||||
Cypress.Commands.add('tabkey', { prevSubject: true }, (subject, { shift, times } = {}) => {
|
||||
const fn = chain => chain.tab({ shift });
|
||||
return runTimes(cy, fn, times).wrap(subject);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => {
|
||||
cy.wrap(subject)
|
||||
.trigger('mousedown')
|
||||
.then(fn)
|
||||
.trigger('mouseup')
|
||||
|
||||
cy.document().trigger('selectionchange');
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('print', { prevSubject: 'optional' }, (subject, str) => {
|
||||
cy.log(str);
|
||||
console.log(`cy.log: ${str}`);
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setSelection', { prevSubject: true }, (subject, query, endQuery) => {
|
||||
return cy.wrap(subject)
|
||||
.selection($el => {
|
||||
if (typeof query === 'string') {
|
||||
const anchorNode = getTextNode($el[0], query);
|
||||
const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode;
|
||||
const anchorOffset = anchorNode.wholeText.indexOf(query);
|
||||
const focusOffset = endQuery ?
|
||||
focusNode.wholeText.indexOf(endQuery) + endQuery.length :
|
||||
anchorOffset + query.length;
|
||||
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
} else if (typeof query === 'object') {
|
||||
const el = $el[0];
|
||||
const anchorNode = getTextNode(el.querySelector(query.anchorQuery));
|
||||
const anchorOffset = query.anchorOffset || 0;
|
||||
const focusNode = query.focusQuery ? getTextNode(el.querySelector(query.focusQuery)) : anchorNode;
|
||||
const focusOffset = query.focusOffset || 0;
|
||||
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setCursor', { prevSubject: true }, (subject, query, atStart) => {
|
||||
return cy.wrap(subject)
|
||||
.selection($el => {
|
||||
const node = getTextNode($el[0], query);
|
||||
const offset = node.wholeText.indexOf(query) + (atStart ? 0 : query.length);
|
||||
const document = node.ownerDocument;
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().collapse(node, offset);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setCursorBefore', { prevSubject: true }, (subject, query) => {
|
||||
cy.wrap(subject).setCursor(query, true);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setCursorAfter', { prevSubject: true }, (subject, query) => {
|
||||
cy.wrap(subject).setCursor(query);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('login', () => {
|
||||
cy.viewport(1200, 1200);
|
||||
cy.visit('/');
|
||||
cy.contains('button', 'Login').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('loginAndNewPost', () => {
|
||||
cy.login();
|
||||
cy.contains('a', 'New Post').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('drag', { prevSubject: true }, subject => {
|
||||
return cy.wrap(subject)
|
||||
.trigger('dragstart', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('drop', { prevSubject: true }, subject => {
|
||||
return cy.wrap(subject)
|
||||
.trigger('drop', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clickToolbarButton', (title, { times } = {}) => {
|
||||
const isHeading = title.startsWith('Heading')
|
||||
if (isHeading) {
|
||||
cy.get('button[title="Headings"]').click();
|
||||
}
|
||||
const instance = isHeading ? cy.contains('div', title) : cy.get(`button[title="${title}"]`);
|
||||
const fn = chain => chain.click();
|
||||
return runTimes(instance, fn, times).focused();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('insertEditorComponent', title => {
|
||||
cy.get('button[title="Add Component"]').click()
|
||||
cy.contains('div', title).click().focused();
|
||||
});
|
||||
|
||||
|
||||
[
|
||||
['clickHeadingOneButton', 'Heading 1'],
|
||||
['clickHeadingTwoButton', 'Heading 2'],
|
||||
['clickOrderedListButton', 'Numbered List'],
|
||||
['clickUnorderedListButton', 'Bulleted List'],
|
||||
['clickCodeButton', 'Code'],
|
||||
['clickItalicButton', 'Italic'],
|
||||
['clickQuoteButton', 'Quote'],
|
||||
].forEach(([commandName, toolbarButtonName]) => {
|
||||
Cypress.Commands.add(commandName, opts => {
|
||||
return cy.clickToolbarButton(toolbarButtonName, opts);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clickModeToggle', () => {
|
||||
cy.get('button[role="switch"]')
|
||||
.click()
|
||||
.focused();
|
||||
});
|
||||
|
||||
[
|
||||
['insertCodeBlock', 'Code Block'],
|
||||
].forEach(([commandName, componentTitle]) => {
|
||||
Cypress.Commands.add(commandName, () => {
|
||||
return cy.insertEditorComponent(componentTitle);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add('getMarkdownEditor', () => {
|
||||
return cy.get('[data-slate-editor]');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('confirmMarkdownEditorContent', expectedDomString => {
|
||||
return cy.getMarkdownEditor()
|
||||
.should(([element]) => {
|
||||
// Slate makes the following representations:
|
||||
// - blank line: 2 BOM's + <br>
|
||||
// - blank element (placed inside empty elements): 1 BOM + <br>
|
||||
// We replace to represent a blank line as a single <br>, and remove the
|
||||
// contents of elements that are actually empty.
|
||||
const actualDomString = toPlainTree(element.innerHTML)
|
||||
.replace(/\uFEFF\uFEFF<br>/g, '<br>')
|
||||
.replace(/\uFEFF<br>/g, '');
|
||||
expect(actualDomString).toEqual(oneLineTrim(expectedDomString));
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clearMarkdownEditorContent', () => {
|
||||
return cy.getMarkdownEditor()
|
||||
.selectAll()
|
||||
.backspace({ times: 2 });
|
||||
});
|
||||
|
||||
function toPlainTree(domString) {
|
||||
return rehype()
|
||||
.use(removeSlateArtifacts)
|
||||
.data('settings', { fragment: true })
|
||||
.processSync(domString)
|
||||
.contents;
|
||||
}
|
||||
|
||||
function getActualBlockChildren(node) {
|
||||
if (node.tagName === 'span') {
|
||||
return node.children.flatMap(getActualBlockChildren);
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: node.children.flatMap(getActualBlockChildren) };
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function removeSlateArtifacts() {
|
||||
return function transform(tree) {
|
||||
visit(tree, 'element', node => {
|
||||
// remove all element attributes
|
||||
delete node.properties;
|
||||
|
||||
// remove slate padding spans to simplify test cases
|
||||
if (['h1', 'p'].includes(node.tagName)) {
|
||||
node.children = node.children.flatMap(getActualBlockChildren);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getTextNode(el, match){
|
||||
const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
|
||||
if (!match) {
|
||||
return walk.nextNode();
|
||||
}
|
||||
|
||||
const nodes = [];
|
||||
let node;
|
||||
while(node = walk.nextNode()) {
|
||||
if (node.wholeText.includes(match)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBaseAndExtent(...args) {
|
||||
const document = args[0].ownerDocument;
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().setBaseAndExtent(...args);
|
||||
}
|
||||
|
@ -12,9 +12,11 @@
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
require('cypress-plugin-tab');
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
import 'cypress-jest-adapter';
|
||||
|
@ -1,7 +1,7 @@
|
||||
const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
|
||||
const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
|
||||
const setting1 = { limit: 10, author: 'John Doe' };
|
||||
const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
|
||||
const setting2 = { name: 'Jane Doe', description: 'description' };
|
||||
const publishTypes = { publishNow: 'Publish now' };
|
||||
const notifications = {
|
||||
saved: 'Entry saved',
|
||||
|
@ -51,16 +51,10 @@ function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) {
|
||||
cy.contains('h2', fromColumnHeading)
|
||||
.parent()
|
||||
.contains('a', title)
|
||||
.trigger('dragstart', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
.drag();
|
||||
cy.contains('h2', toColumnHeading)
|
||||
.parent()
|
||||
.trigger('drop', {
|
||||
dataTransfer: {},
|
||||
force: true,
|
||||
});
|
||||
.drop();
|
||||
assertNotification(notifications.updated);
|
||||
}
|
||||
|
||||
@ -171,7 +165,7 @@ function populateEntry(entry) {
|
||||
for (let key of keys) {
|
||||
const value = entry[key];
|
||||
if (key === 'body') {
|
||||
cy.get('[data-slate-editor]')
|
||||
cy.getMarkdownEditor()
|
||||
.click()
|
||||
.clear()
|
||||
.type(value);
|
||||
@ -288,7 +282,7 @@ function validateListFields({ name, description }) {
|
||||
cy.get('input')
|
||||
.eq(2)
|
||||
.type(name);
|
||||
cy.get('[data-slate-editor]')
|
||||
cy.getMarkdownEditor()
|
||||
.eq(2)
|
||||
.type(description);
|
||||
cy.contains('button', 'Save').click();
|
||||
|
Reference in New Issue
Block a user