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'); }); });