import { fixture, expect } from '@open-wc/testing'; import { html } from 'lit'; import '../index'; import { TextSelection } from 'prosemirror-state'; import type { NileWysiwygEditor } from './nile-wysiwyg-editor'; async function editorFixture(template = html``) { const el = await fixture(template); await el.updateComplete; return el; } /** A compact table-based email with cell padding and a 600px container. */ const EMAIL_FIXTURE = '' + '
' + '' + '' + '' + '
' + '

Company Name

' + '
' + '

Hello there.

' + '
'; /** A full standalone HTML document, the kind that lands in text/plain. */ const FULL_DOC = '' + '
Pasted cell
' + ''; /** Fire a paste through the ProseMirror view's handlePaste prop. */ function firePaste(el: NileWysiwygEditor, data: Record) { const view = el.pmView!; const dt = new DataTransfer(); for (const [type, value] of Object.entries(data)) dt.setData(type, value); const event = new ClipboardEvent('paste', { clipboardData: dt }); return view.someProp('handlePaste', f => f(view, event, view.state.tr.doc.slice(0))); } describe('nile-wysiwyg-editor email-fidelity: property reflection', () => { it('1. defaults emailFidelity to false', async () => { const el = await editorFixture(); expect(el.emailFidelity).to.equal(false); expect(el.hasAttribute('email-fidelity')).to.equal(false); }); it('2. reflects the property to the kebab attribute', async () => { const el = await editorFixture(); el.emailFidelity = true; await el.updateComplete; expect(el.hasAttribute('email-fidelity')).to.equal(true); }); it('3. reads the attribute into the property', async () => { const el = await editorFixture( html`` ); expect(el.emailFidelity).to.equal(true); }); }); describe('nile-wysiwyg-editor email-fidelity: value parsing', () => { it('4. keeps cell padding in value with the flag on', async () => { const el = await editorFixture( html`` ); el.value = EMAIL_FIXTURE; await el.updateComplete; const doc = new DOMParser().parseFromString(el.value, 'text/html'); const padded = Array.from(doc.querySelectorAll('td')).some( td => td.style.padding !== '' || td.style.paddingTop !== '' ); expect(padded, 'a td keeps padding').to.equal(true); }); it('5. strips cell padding in value with the flag off', async () => { const el = await editorFixture(); el.value = EMAIL_FIXTURE; await el.updateComplete; expect(el.value).to.not.include('padding'); }); it('6. re-parses value when the flag toggles on', async () => { const el = await editorFixture(); el.value = EMAIL_FIXTURE; await el.updateComplete; expect(el.value).to.not.include('padding'); el.emailFidelity = true; el.value = EMAIL_FIXTURE; await el.updateComplete; expect(el.value).to.match(/padding/); }); }); describe('nile-wysiwyg-editor email-fidelity: paste', () => { it('7. parses a text/plain-only HTML document when the flag is on', async () => { const el = await editorFixture( html`` ); const handled = firePaste(el, { 'text/plain': FULL_DOC }); expect(handled).to.equal(true); await el.updateComplete; expect(el.getHTML()).to.include(' { const el = await editorFixture(); firePaste(el, { 'text/plain': FULL_DOC }); await el.updateComplete; // No real table node — the markup is kept as literal text. expect(el.getHTML()).to.not.include(' { const el = await editorFixture( html`` ); firePaste(el, { 'text/plain': FULL_DOC }); await el.updateComplete; expect(el.getHTML()).to.not.include(' { const el = await editorFixture( html`` ); firePaste(el, { 'text/plain': '# Heading\n\nSome **bold** text.' }); await el.updateComplete; const out = el.getHTML(); expect(out).to.include('

'); expect(out).to.include(''); }); }); /** Focuses the editor with the caret inside the first text that equals `text`. */ async function caretIn(el: NileWysiwygEditor, text: string) { const view = el.pmView!; let pos = -1; view.state.doc.descendants((node, at) => { if (pos === -1 && node.isText && node.text === text) pos = at + 1; return pos === -1; }); view.focus(); view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, pos))); await el.updateComplete; await new Promise(r => setTimeout(r, 50)); } function tableMenuOpen(el: NileWysiwygEditor): boolean { return !!el.shadowRoot!.querySelector('nile-wysiwyg-table-menu')!.hasAttribute('open'); } const DATA_TABLE = '
data cell
'; const LAYOUT_TABLE = '

layout cell

'; const DATA_IN_LAYOUT = '
inner data
'; describe('nile-wysiwyg-editor: table menu visibility', () => { it('11. shows in a data table by default', async () => { const el = await editorFixture(html``); await caretIn(el, 'data cell'); expect(tableMenuOpen(el)).to.equal(true); }); it('12. no-table-menu hides it', async () => { const el = await editorFixture( html`` ); expect(el.noTableMenu).to.equal(true); await caretIn(el, 'data cell'); expect(tableMenuOpen(el)).to.equal(false); }); it('13. toggling noTableMenu while focused closes and reopens it', async () => { const el = await editorFixture(html``); await caretIn(el, 'data cell'); el.noTableMenu = true; await el.updateComplete; expect(tableMenuOpen(el)).to.equal(false); el.noTableMenu = false; await el.updateComplete; expect(tableMenuOpen(el)).to.equal(true); }); it('14. email-fidelity hides it inside a layout table', async () => { const el = await editorFixture( html`` ); await caretIn(el, 'layout cell'); expect(tableMenuOpen(el)).to.equal(false); }); it('15. without email-fidelity a layout table still shows it (unchanged)', async () => { const el = await editorFixture(html``); await caretIn(el, 'layout cell'); expect(tableMenuOpen(el)).to.equal(true); }); it('16. email-fidelity still shows it in a data table nested in a layout table', async () => { const el = await editorFixture( html`` ); await caretIn(el, 'inner data'); expect(tableMenuOpen(el)).to.equal(true); }); }); describe('nile-wysiwyg-editor email-fidelity: saved HTML matches the editor', () => { /** A table with no role="presentation" and no layout styles of its own. */ const DATA_TABLE = '

Employee Number:

1342

'; const AUTHORED = '
' + '

Kept

'; const LAYOUT = '

layout cell

'; const parse = (out: string) => new DOMParser().parseFromString(out, 'text/html').body; async function load(value: string, fidelity = true) { const el = await editorFixture( fidelity ? html`` : html`` ); el.value = value; await el.updateComplete; return el; } it('1. writes the editor table rendering into a data table that sets none', async () => { const el = await load(DATA_TABLE); const body = parse(el.getHTML()); const table = body.querySelector('table')!; expect(table.getAttribute('cellpadding')).to.equal('0'); expect(table.getAttribute('cellspacing')).to.equal('0'); expect(table.style.width).to.equal('100%'); expect(table.style.borderCollapse).to.equal('collapse'); expect(table.style.tableLayout).to.equal('fixed'); const td = body.querySelector('td')!; expect(td.style.padding).to.equal('6px 8px'); expect(td.style.verticalAlign).to.equal('top'); const p = body.querySelector('p')!; expect(p.style.marginTop).to.equal('0px'); expect(p.style.marginBottom).to.equal('0px'); }); it('2. keeps values the content sets itself', async () => { const el = await load(AUTHORED); const body = parse(el.getHTML()); expect(body.querySelector('table')!.style.width).to.equal('600px'); const td = body.querySelector('td')!; expect(td.style.padding).to.equal('20px'); expect(td.style.verticalAlign).to.equal('middle'); expect(body.querySelector('p')!.style.margin).to.equal('0px 0px 12px'); }); it('3. leaves layout-table cells as authored, adding only cellpadding/cellspacing and collapse', async () => { const el = await load(LAYOUT); const body = parse(el.getHTML()); const table = body.querySelector('table')!; expect(table.getAttribute('cellpadding')).to.equal('0'); expect(table.style.borderCollapse).to.equal('collapse'); expect(table.style.tableLayout).to.equal(''); expect(table.style.width).to.equal(''); const td = body.querySelector('td')!; expect(td.style.padding).to.equal(''); expect(td.style.verticalAlign).to.equal(''); }); it('4. without email-fidelity the output is unchanged', async () => { const el = await load(DATA_TABLE, false); const out = el.getHTML(); expect(out).to.not.include('cellpadding'); expect(out).to.not.include('padding'); expect(out).to.not.include('border-collapse'); }); it('5. is stable across save and reload', async () => { const first = await load(DATA_TABLE); const saved = first.getHTML(); const second = await load(saved); expect(second.getHTML()).to.equal(saved); expect(second.value).to.equal(saved); }); it('6. writes cell borders and the header fill in the colours the theme draws', async () => { const el = await editorFixture( html`` ); el.value = '

Attribute

Email

'; await el.updateComplete; const body = parse(el.getHTML()); const td = body.querySelector('td')!; expect(td.style.borderTop).to.equal('1px solid rgb(1, 2, 3)'); expect(td.style.borderLeft).to.equal('1px solid rgb(1, 2, 3)'); const th = body.querySelector('th')!; expect(th.style.backgroundColor).to.equal('rgb(4, 5, 6)'); expect(th.style.textAlign).to.equal('left'); expect(th.style.fontWeight).to.equal('600'); const again = await load(el.getHTML()); expect(parse(again.getHTML()).querySelector('td')!.style.borderTop).to.equal( '1px solid rgb(1, 2, 3)' ); }); it('6b. keeps an imported cell border across two saves', async () => { const first = await load( '

x

' ); const second = await load(first.getHTML()); const td = parse(second.getHTML()).querySelector('td')!; expect(td.style.borderTop).to.equal('1px solid rgb(217, 222, 228)'); expect(td.style.borderRight).to.equal('1px solid rgb(217, 222, 228)'); expect(second.getHTML()).to.equal(first.getHTML()); }); it('7. writes no border when the theme draws none', async () => { const el = await load(DATA_TABLE); expect(el.getHTML()).to.not.include('border-top'); }); it('8. writes a resized column width onto its cells', async () => { const el = await load( '

a

b

' ); const tds = parse(el.getHTML()).querySelectorAll('td'); expect((tds[0] as HTMLElement).style.width).to.equal('120px'); expect((tds[1] as HTMLElement).style.width).to.equal(''); }); it('9. draws layout-table cells the way a mail client does, data cells as before', async () => { const el = await load(LAYOUT + DATA_TABLE); const cells = [...el.shadowRoot!.querySelectorAll('.ProseMirror td')] as HTMLElement[]; const layoutCell = cells.find(c => c.textContent === 'layout cell')!; const dataCell = cells.find(c => c.textContent === '1342')!; expect(getComputedStyle(layoutCell).padding).to.equal('0px'); expect(getComputedStyle(layoutCell).verticalAlign).to.equal('middle'); expect(getComputedStyle(dataCell).padding).to.equal('6px 8px'); expect(getComputedStyle(dataCell).verticalAlign).to.equal('top'); }); it('10. adds no editor-only margin around tables', async () => { const el = await load(DATA_TABLE); const wrapper = el.shadowRoot!.querySelector('.ProseMirror .tableWrapper') as HTMLElement; expect(getComputedStyle(wrapper).marginTop).to.equal('0px'); }); });