2025-05-09 22:35:36 +03:00
|
|
|
import { ClassicEditor, Essentials, Paragraph, Heading, CodeBlockEditing, _setModelData as setModelData, _getModelData as getModelData, _getViewData as getViewData } from 'ckeditor5';
|
2024-05-20 14:27:21 +02:00
|
|
|
import MermaidEditing from '../src/mermaidediting.js';
|
2025-05-10 02:23:22 +03:00
|
|
|
import { afterEach, beforeEach, describe, it } from 'vitest';
|
|
|
|
import { expect } from 'vitest';
|
|
|
|
import { vi } from 'vitest';
|
2022-03-04 13:39:39 +01:00
|
|
|
|
|
|
|
/* global document */
|
|
|
|
|
|
|
|
describe( 'MermaidEditing', () => {
|
|
|
|
it( 'should be named', () => {
|
|
|
|
expect( MermaidEditing.pluginName ).to.equal( 'MermaidEditing' );
|
|
|
|
} );
|
|
|
|
|
|
|
|
describe( 'conversion', () => {
|
|
|
|
let domElement, editor, model;
|
|
|
|
|
|
|
|
beforeEach( async () => {
|
|
|
|
domElement = document.createElement( 'div' );
|
|
|
|
document.body.appendChild( domElement );
|
|
|
|
|
|
|
|
editor = await ClassicEditor.create( domElement, {
|
2025-05-10 02:25:11 +03:00
|
|
|
licenseKey: "GPL",
|
2022-03-04 13:39:39 +01:00
|
|
|
plugins: [
|
|
|
|
Paragraph,
|
|
|
|
Heading,
|
|
|
|
Essentials,
|
|
|
|
CodeBlockEditing,
|
|
|
|
MermaidEditing
|
|
|
|
]
|
|
|
|
} );
|
|
|
|
|
|
|
|
model = editor.model;
|
|
|
|
} );
|
|
|
|
|
|
|
|
afterEach( () => {
|
|
|
|
domElement.remove();
|
|
|
|
return editor.destroy();
|
|
|
|
} );
|
|
|
|
|
|
|
|
describe( 'conversion', () => {
|
|
|
|
describe( 'upcast', () => {
|
|
|
|
it( 'works correctly', () => {
|
|
|
|
editor.setData(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
|
|
|
|
'<mermaid displayMode="split" source="flowchart TB\nA --> B\nB --> C">' +
|
|
|
|
'</mermaid>'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'works correctly when empty', () => {
|
|
|
|
editor.setData(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid"></code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
|
|
|
|
'<mermaid displayMode="split" source=""></mermaid>'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
describe( 'data downcast', () => {
|
|
|
|
it( 'works correctly', () => {
|
|
|
|
// Using editor.setData() instead of setModelData helper because of #11365.
|
|
|
|
editor.setData(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( editor.getData() ).to.equal(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'works correctly when empty ', () => {
|
|
|
|
// Using editor.setData() instead of setModelData helper because of #11365.
|
|
|
|
editor.setData(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid"></code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( editor.getData() ).to.equal(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid"></code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
describe( 'editing downcast', () => {
|
|
|
|
it( 'works correctly without displayMode attribute', () => {
|
|
|
|
// Using editor.setData() instead of setModelData helper because of #11365.
|
|
|
|
editor.setData(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
|
|
|
|
'<div class="ck-mermaid__split-mode ck-mermaid__wrapper ck-widget ck-widget_selected' +
|
|
|
|
' ck-widget_with-selection-handle" contenteditable="false">' +
|
|
|
|
'<div class="ck ck-widget__selection-handle"></div>' +
|
|
|
|
// New lines replaced with space, same issue in getViewData as in #11365.
|
|
|
|
'<textarea class="ck-mermaid__editing-view" data-cke-ignore-events="true"' +
|
|
|
|
' placeholder="Insert mermaid source code"></textarea>' +
|
|
|
|
'<div class="ck-mermaid__preview"></div>' +
|
|
|
|
'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
|
|
|
|
'</div>'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'works correctly with displayMode attribute', () => {
|
|
|
|
setModelData( editor.model,
|
|
|
|
'<mermaid source="foo" displayMode="preview"></mermaid>'
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
|
|
|
|
'<div class="ck-mermaid__preview-mode ck-mermaid__wrapper ck-widget ck-widget_selected ' +
|
|
|
|
'ck-widget_with-selection-handle" contenteditable="false">' +
|
|
|
|
'<div class="ck ck-widget__selection-handle"></div>' +
|
|
|
|
'<textarea class="ck-mermaid__editing-view" data-cke-ignore-events="true"' +
|
|
|
|
' placeholder="Insert mermaid source code"></textarea>' +
|
|
|
|
'<div class="ck-mermaid__preview"></div>' +
|
|
|
|
'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
|
|
|
|
'</div>'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'works correctly with empty source', () => {
|
|
|
|
setModelData( editor.model,
|
|
|
|
'<mermaid source="" displayMode="preview"></mermaid>'
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
|
|
|
|
'<div class="ck-mermaid__preview-mode ck-mermaid__wrapper ck-widget ck-widget_selected ' +
|
|
|
|
'ck-widget_with-selection-handle" contenteditable="false">' +
|
|
|
|
'<div class="ck ck-widget__selection-handle"></div>' +
|
|
|
|
'<textarea class="ck-mermaid__editing-view" data-cke-ignore-events="true"' +
|
|
|
|
' placeholder="Insert mermaid source code"></textarea>' +
|
|
|
|
'<div class="ck-mermaid__preview"></div>' +
|
|
|
|
'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
|
|
|
|
'</div>'
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
describe( 'textarea value', () => {
|
|
|
|
let domTextarea = null;
|
|
|
|
|
|
|
|
beforeEach( () => {
|
|
|
|
// Using editor.setData() instead of setModelData helper because of #11365.
|
|
|
|
editor.setData(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
|
|
|
|
const textareaView = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 1 );
|
|
|
|
domTextarea = editor.editing.view.domConverter.viewToDom( textareaView );
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'is properly set during the initial conversion', () => {
|
|
|
|
expect( domTextarea.value ).to.equal( 'flowchart TB\nA --> B\nB --> C' );
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'is properly updated after model\'s attribute change', () => {
|
|
|
|
const { model } = editor;
|
|
|
|
|
|
|
|
const mermaidModel = model.document.getRoot().getChild( 0 );
|
|
|
|
|
|
|
|
model.change( writer => {
|
|
|
|
writer.setAttribute( 'source', 'abc', mermaidModel );
|
|
|
|
} );
|
|
|
|
|
|
|
|
expect( domTextarea.value ).to.equal( 'abc' );
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'doesn\'t loop if model attribute changes to the same value', () => {
|
|
|
|
const { model } = editor;
|
|
|
|
|
|
|
|
const mermaidModel = model.document.getRoot().getChild( 0 );
|
|
|
|
|
|
|
|
model.change( writer => {
|
|
|
|
writer.setAttribute( 'source', 'flowchart TB\nA --> B\nB --> C', mermaidModel );
|
|
|
|
} );
|
|
|
|
|
|
|
|
expect( domTextarea.value ).to.equal( 'flowchart TB\nA --> B\nB --> C' );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
describe( 'preview div', () => {
|
|
|
|
let domPreviewContainer, renderMermaidStub;
|
|
|
|
|
|
|
|
beforeEach( () => {
|
|
|
|
// Using editor.setData() instead of setModelData helper because of #11365.
|
|
|
|
editor.setData(
|
|
|
|
'<pre spellcheck="false">' +
|
|
|
|
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
|
|
|
|
'</pre>'
|
|
|
|
);
|
|
|
|
|
|
|
|
const previewContainerView = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 2 );
|
|
|
|
domPreviewContainer = editor.editing.view.domConverter.viewToDom( previewContainerView );
|
|
|
|
|
2025-05-09 23:56:09 +03:00
|
|
|
renderMermaidStub = vi.spyOn( editor.plugins.get( 'MermaidEditing' ), '_renderMermaid' );
|
2022-03-04 13:39:39 +01:00
|
|
|
} );
|
|
|
|
|
|
|
|
afterEach( () => {
|
2025-05-10 02:31:29 +03:00
|
|
|
vi.clearAllMocks();
|
2022-03-04 13:39:39 +01:00
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'has proper inner text set during the initial conversion', () => {
|
|
|
|
expect( domPreviewContainer.textContent ).to.equal( 'flowchart TB\nA --> B\nB --> C' );
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'has proper inner text set after a model\'s attribute change', () => {
|
|
|
|
const { model } = editor;
|
|
|
|
|
|
|
|
const mermaidModel = model.document.getRoot().getChild( 0 );
|
|
|
|
|
|
|
|
model.change( writer => {
|
|
|
|
writer.setAttribute( 'source', 'abc', mermaidModel );
|
|
|
|
} );
|
|
|
|
|
|
|
|
expect( domPreviewContainer.textContent ).to.equal( 'abc' );
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'calls mermaid render function after a model\'s attribute change', () => {
|
|
|
|
const { model } = editor;
|
|
|
|
|
|
|
|
const mermaidModel = model.document.getRoot().getChild( 0 );
|
|
|
|
|
|
|
|
model.change( writer => {
|
|
|
|
writer.setAttribute( 'source', 'abc', mermaidModel );
|
|
|
|
} );
|
|
|
|
|
2025-05-10 02:23:22 +03:00
|
|
|
expect(renderMermaidStub).toBeCalledWith(domPreviewContainer);
|
2022-03-04 13:39:39 +01:00
|
|
|
} );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'adds a editing pipeline converter that has a precedence over code block', () => {
|
|
|
|
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
|
|
|
|
|
|
|
|
const firstViewChild = editor.editing.view.document.getRoot().getChild( 0 );
|
|
|
|
|
|
|
|
expect( firstViewChild.name ).to.equal( 'div' );
|
|
|
|
expect( firstViewChild.hasClass( 'ck-mermaid__wrapper' ), 'has ck-mermaid__wrapper class' ).to.be.true;
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'does not convert code blocks other than mermaid language', () => {
|
|
|
|
setModelData( editor.model, '<codeBlock language="javascript">foo</codeBlock>' );
|
|
|
|
|
|
|
|
const firstViewChild = editor.editing.view.document.getRoot().getChild( 0 );
|
|
|
|
|
|
|
|
expect( firstViewChild.name ).not.to.equal( 'div' );
|
|
|
|
expect( firstViewChild.hasClass( 'ck-mermaid__wrapper' ), 'has ck-mermaid__wrapper class' ).to.be.false;
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'adds a preview element', () => {
|
|
|
|
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
|
|
|
|
|
|
|
|
const widgetChildren = [ ...editor.editing.view.document.getRoot().getChild( 0 ).getChildren() ];
|
|
|
|
const previewView = widgetChildren.filter( item => item.name === 'div' && item.hasClass( 'ck-mermaid__preview' ) );
|
|
|
|
|
|
|
|
expect( previewView.length ).to.equal( 1 );
|
|
|
|
} );
|
|
|
|
|
|
|
|
it( 'adds an editing element', () => {
|
|
|
|
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
|
|
|
|
|
|
|
|
const widgetChildren = [ ...editor.editing.view.document.getRoot().getChild( 0 ).getChildren() ];
|
|
|
|
const previewView = widgetChildren.filter(
|
|
|
|
item => item.name === 'textarea' && item.hasClass( 'ck-mermaid__editing-view' )
|
|
|
|
);
|
|
|
|
|
|
|
|
expect( previewView.length ).to.equal( 1 );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
} );
|