import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import './nile-markdown-editor';
import NileMarkdownEditor from './nile-markdown-editor';
const getTextarea = (el: NileMarkdownEditor) =>
el.shadowRoot!.querySelector('textarea')!;
describe('NileMarkdownEditor', () => {
// === RENDERING ===
it('1. should render without errors', async () => {
const el = await fixture(
html``
);
expect(el).to.exist;
});
it('2. should have a shadow root', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot).to.not.be.null;
});
it('3. should render a textarea in write mode', async () => {
const el = await fixture(
html``
);
expect(getTextarea(el)).to.exist;
});
it('4. should have base, header, toolbar, textarea parts', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot!.querySelector('[part~="base"]')).to.exist;
expect(el.shadowRoot!.querySelector('[part~="header"]')).to.exist;
expect(el.shadowRoot!.querySelector('[part~="toolbar"]')).to.exist;
expect(el.shadowRoot!.querySelector('[part~="textarea"]')).to.exist;
});
it('5. should be instance of NileMarkdownEditor', async () => {
const el = await fixture(
html``
);
expect(el).to.be.instanceOf(NileMarkdownEditor);
});
// === DEFAULT PROPERTIES ===
it('6. should default to write mode', async () => {
const el = await fixture(
html``
);
expect(el.mode).to.equal('write');
});
it('7. should default value to empty string', async () => {
const el = await fixture(
html``
);
expect(el.value).to.equal('');
});
it('8. should default rows to 8', async () => {
const el = await fixture(
html``
);
expect(el.rows).to.equal(8);
});
// === VALUE ===
it('9. should pass value to the textarea', async () => {
const el = await fixture(
html``
);
expect(getTextarea(el).value).to.equal('# Hi');
});
it('10. should update value on textarea input', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.value = 'typed';
ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
expect(el.value).to.equal('typed');
});
// === MODES ===
it('11. should show preview pane in preview mode', async () => {
const el = await fixture(
html``
);
const preview = el.shadowRoot!.querySelector('[part~="preview"]');
expect(preview).to.exist;
expect(el.shadowRoot!.querySelector('textarea')).to.not.exist;
});
it('12. should render markdown in the preview', async () => {
const el = await fixture(
html``
);
const md = el.shadowRoot!.querySelector('nile-markdown')!;
await md.updateComplete;
expect(md.shadowRoot!.querySelector('h1')!.textContent).to.equal('Title');
});
it('13. should show both panes in split mode', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot!.querySelector('textarea')).to.exist;
expect(el.shadowRoot!.querySelector('[part~="preview"]')).to.exist;
});
it('14. should switch mode via tab click and emit nile-mode-change', async () => {
const el = await fixture(
html``
);
const tabs = el.shadowRoot!.querySelectorAll('nile-button-toggle');
setTimeout(() => (tabs[1] as HTMLElement).click());
const event = await oneEvent(el, 'nile-mode-change');
expect(event.detail.mode).to.equal('preview');
expect(el.mode).to.equal('preview');
});
it('15. should show empty preview placeholder', async () => {
const el = await fixture(
html``
);
const empty = el.shadowRoot!.querySelector('.preview-empty');
expect(empty).to.exist;
});
// === TOOLBAR ===
it('16. should render toolbar buttons', async () => {
const el = await fixture(
html``
);
const buttons = el.shadowRoot!.querySelectorAll('.toolbar__button');
expect(buttons.length).to.equal(9);
});
it('17. should hide toolbar with hide-toolbar', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot!.querySelector('[part~="toolbar"]')).to.not.exist;
});
it('18. bold button should wrap the selection', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.setSelectionRange(0, 5);
const bold =
el.shadowRoot!.querySelector('[title^="Bold"]')!;
bold.click();
expect(el.value).to.equal('**hello** world');
});
it('19. bold button should unwrap an already-bold selection', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.setSelectionRange(2, 7); // "hello"
el.shadowRoot!.querySelector('[title^="Bold"]')!.click();
expect(el.value).to.equal('hello world');
});
it('20. bold button should insert placeholder without selection', async () => {
const el = await fixture(
html``
);
getTextarea(el).setSelectionRange(0, 0);
el.shadowRoot!.querySelector('[title^="Bold"]')!.click();
expect(el.value).to.equal('**bold text**');
});
it('21. heading button should prefix the line', async () => {
const el = await fixture(
html``
);
getTextarea(el).setSelectionRange(0, 0);
el.shadowRoot!.querySelector(
'[title="Heading"]'
)!.click();
expect(el.value).to.equal('### Title');
});
it('22. heading button should toggle the prefix off', async () => {
const el = await fixture(
html``
);
getTextarea(el).setSelectionRange(0, 0);
el.shadowRoot!.querySelector(
'[title="Heading"]'
)!.click();
expect(el.value).to.equal('Title');
});
it('23. bulleted list should prefix every selected line', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.setSelectionRange(0, ta.value.length);
el.shadowRoot!.querySelector(
'[title="Bulleted list"]'
)!.click();
expect(el.value).to.equal('- one\n- two\n- three');
});
it('24. numbered list should number every selected line', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.setSelectionRange(0, ta.value.length);
el.shadowRoot!.querySelector(
'[title="Numbered list"]'
)!.click();
expect(el.value).to.equal('1. one\n2. two');
});
it('25. link button should insert a markdown link', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.setSelectionRange(0, 4);
el.shadowRoot!.querySelector('[title^="Link"]')!.click();
expect(el.value).to.equal('[docs](url)');
});
// === EVENTS ===
it('26. should emit nile-input on typing', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
setTimeout(() => {
ta.value = 'abc';
ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
});
const event = await oneEvent(el, 'nile-input');
expect(event.detail.value).to.equal('abc');
});
it('27. should emit nile-input on toolbar action', async () => {
const el = await fixture(
html``
);
setTimeout(() =>
el
.shadowRoot!.querySelector('[title^="Bold"]')!
.click()
);
const event = await oneEvent(el, 'nile-input');
expect(event.detail.value).to.equal('**bold text**');
});
it('28. should emit nile-change on textarea change', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
setTimeout(() => {
ta.value = 'done';
ta.dispatchEvent(new InputEvent('input', { bubbles: true }));
ta.dispatchEvent(new Event('change', { bubbles: true }));
});
const event = await oneEvent(el, 'nile-change');
expect(event.detail.value).to.equal('done');
});
// === KEYBOARD SHORTCUTS ===
it('29. Ctrl+B should bold the selection', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.setSelectionRange(0, 2);
ta.dispatchEvent(
new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true })
);
expect(el.value).to.equal('**hi**');
});
// === DISABLED / READONLY ===
it('30. should disable the textarea when disabled', async () => {
const el = await fixture(
html``
);
expect(getTextarea(el).disabled).to.be.true;
});
it('31. should make the textarea readonly when readonly', async () => {
const el = await fixture(
html``
);
expect(getTextarea(el).readOnly).to.be.true;
});
it('32. toolbar actions should not modify a readonly editor', async () => {
const el = await fixture(
html``
);
el.shadowRoot!.querySelector('[title^="Bold"]')!.click();
expect(el.value).to.equal('text');
});
// === MISC ===
it('33. should reflect mode attribute', async () => {
const el = await fixture(
html``
);
expect(el.getAttribute('mode')).to.equal('split');
});
it('34. should apply placeholder to the textarea', async () => {
const el = await fixture(
html``
);
expect(getTextarea(el).placeholder).to.equal('Type here');
});
it('35. should have static styles', () => {
expect(NileMarkdownEditor.styles).to.exist;
});
// === TOOLS ALLOWLIST ===
it('36. should show all tools by default', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot!.querySelectorAll('.toolbar__button').length).to.equal(9);
});
it('37. should only show allowlisted tools', async () => {
const el = await fixture(
html``
);
const buttons = el.shadowRoot!.querySelectorAll('.toolbar__button');
expect(buttons.length).to.equal(3);
expect(el.shadowRoot!.querySelector('[title^="Bold"]')).to.exist;
expect(el.shadowRoot!.querySelector('[title^="Italic"]')).to.exist;
expect(el.shadowRoot!.querySelector('[title^="Link"]')).to.exist;
expect(el.shadowRoot!.querySelector('[title^="Heading"]')).to.not.exist;
});
it('38. should drop empty groups (no dangling dividers)', async () => {
// "link" is alone in the last group; "bold" is in the first group.
const el = await fixture(
html``
);
const dividers = el.shadowRoot!.querySelectorAll('.toolbar__divider');
// Two non-empty groups => exactly one divider between them.
expect(dividers.length).to.equal(1);
});
it('39. should hide the whole toolbar when no allowlisted tool matches', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot!.querySelector('[part~="toolbar"]')).to.not.exist;
});
it('40. should disable a tool keyboard shortcut when it is not allowlisted', async () => {
const el = await fixture(
html``
);
const ta = getTextarea(el);
ta.setSelectionRange(0, 5);
// Ctrl+B is bold, which is NOT allowlisted -> no change.
ta.dispatchEvent(
new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true })
);
expect(el.value).to.equal('hello world');
});
});