2026-04-28 23:08:20 -07:00
|
|
|
import { ribbit, resetDOM } from './setup';
|
|
|
|
|
|
2026-04-29 15:48:36 -07:00
|
|
|
const lib = ribbit();
|
2026-04-28 23:08:20 -07:00
|
|
|
|
|
|
|
|
describe('ToolbarManager', () => {
|
|
|
|
|
beforeEach(() => resetDOM('**bold** text'));
|
|
|
|
|
|
|
|
|
|
describe('button registration', () => {
|
|
|
|
|
it('registers tag buttons', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('bold')).toBeDefined();
|
|
|
|
|
expect(editor.toolbar.buttons.get('italic')).toBeDefined();
|
|
|
|
|
expect(editor.toolbar.buttons.get('code')).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('registers editor actions', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('save')).toBeDefined();
|
|
|
|
|
expect(editor.toolbar.buttons.get('toggle')).toBeDefined();
|
|
|
|
|
expect(editor.toolbar.buttons.get('markdown')).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('registers macro buttons', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({
|
|
|
|
|
macros: [{
|
|
|
|
|
name: 'user',
|
|
|
|
|
toHTML: () => 'u',
|
|
|
|
|
}],
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('macro:user')).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips macros with button: false', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({
|
|
|
|
|
macros: [{
|
|
|
|
|
name: 'hidden',
|
|
|
|
|
toHTML: () => '',
|
|
|
|
|
button: false,
|
|
|
|
|
}],
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('macro:hidden')).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips tags without button', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('paragraph')).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('button properties', () => {
|
|
|
|
|
it('bold has correct label and shortcut', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
const bold = editor.toolbar.buttons.get('bold')!;
|
|
|
|
|
expect(bold.label).toBe('Bold');
|
|
|
|
|
expect(bold.shortcut).toBe('Ctrl+B');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('bold action is wrap', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('bold')!.action).toBe('wrap');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('save action is custom', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('save')!.action).toBe('custom');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('table has template', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
const table = editor.toolbar.buttons.get('table')!;
|
|
|
|
|
expect(table.template).toContain('Header');
|
|
|
|
|
expect(table.replaceSelection).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('macro button has insert action', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({
|
|
|
|
|
macros: [{
|
|
|
|
|
name: 'toc',
|
|
|
|
|
toHTML: () => '',
|
|
|
|
|
}],
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
editor.run();
|
|
|
|
|
const btn = editor.toolbar.buttons.get('macro:toc')!;
|
|
|
|
|
expect(btn.action).toBe('insert');
|
|
|
|
|
expect(btn.template).toBe('@toc');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('button.hide() and button.show()', () => {
|
|
|
|
|
it('hide sets visible false', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
const bold = editor.toolbar.buttons.get('bold')!;
|
|
|
|
|
expect(bold.visible).toBe(true);
|
|
|
|
|
bold.hide();
|
|
|
|
|
expect(bold.visible).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('show restores visible', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
const bold = editor.toolbar.buttons.get('bold')!;
|
|
|
|
|
bold.hide();
|
|
|
|
|
bold.show();
|
|
|
|
|
expect(bold.visible).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('render()', () => {
|
|
|
|
|
it('returns an HTMLElement', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
expect(toolbar.tagName).toBe('NAV');
|
|
|
|
|
expect(toolbar.className).toBe('ribbit-toolbar');
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('contains buttons', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
expect(toolbar.querySelector('.ribbit-btn-bold')).not.toBeNull();
|
|
|
|
|
expect(toolbar.querySelector('.ribbit-btn-save')).not.toBeNull();
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('buttons have aria-label', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
const bold = toolbar.querySelector('.ribbit-btn-bold');
|
2026-04-28 23:08:20 -07:00
|
|
|
expect(bold?.getAttribute('aria-label')).toBe('Bold');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('buttons have title with shortcut', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
const bold = toolbar.querySelector('.ribbit-btn-bold');
|
2026-04-28 23:08:20 -07:00
|
|
|
expect(bold?.getAttribute('title')).toBe('Bold (Ctrl+B)');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders spacers', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({
|
2026-04-28 23:08:20 -07:00
|
|
|
autoToolbar: false,
|
|
|
|
|
toolbar: ['bold', '', 'save'],
|
|
|
|
|
});
|
|
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
expect(toolbar.querySelector('.spacer')).not.toBeNull();
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders dropdown groups', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({
|
2026-04-28 23:08:20 -07:00
|
|
|
autoToolbar: false,
|
2026-04-29 15:48:36 -07:00
|
|
|
toolbar: [{
|
|
|
|
|
group: 'Test',
|
|
|
|
|
items: ['bold', 'italic'],
|
|
|
|
|
}],
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
expect(toolbar.querySelector('.ribbit-dropdown')).not.toBeNull();
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('auto-render', () => {
|
|
|
|
|
it('inserts toolbar before editor by default', () => {
|
|
|
|
|
resetDOM();
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({});
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbarElement = editor.element.previousElementSibling;
|
|
|
|
|
expect(toolbarElement?.className).toBe('ribbit-toolbar');
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not insert when autoToolbar is false', () => {
|
|
|
|
|
resetDOM();
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbarElement = editor.element.previousElementSibling;
|
|
|
|
|
expect(toolbarElement?.className || '').not.toBe('ribbit-toolbar');
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('custom layout', () => {
|
|
|
|
|
it('respects custom toolbar order', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({
|
2026-04-28 23:08:20 -07:00
|
|
|
autoToolbar: false,
|
|
|
|
|
toolbar: ['save', 'bold'],
|
|
|
|
|
});
|
|
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
const buttons = toolbar.querySelectorAll('button');
|
2026-04-28 23:08:20 -07:00
|
|
|
expect(buttons[0]?.className).toBe('ribbit-btn-save');
|
|
|
|
|
expect(buttons[1]?.className).toBe('ribbit-btn-bold');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('auto-generates layout when not specified', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
|
|
|
|
expect(toolbar.querySelectorAll('button').length).toBeGreaterThan(3);
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('enable/disable', () => {
|
|
|
|
|
it('disable adds disabled class', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.toolbar.disable();
|
2026-04-29 15:48:36 -07:00
|
|
|
const bold = toolbar.querySelector('.ribbit-btn-bold');
|
2026-04-28 23:08:20 -07:00
|
|
|
expect(bold?.classList.contains('disabled')).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('enable removes disabled class', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
const toolbar = editor.toolbar.render();
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.toolbar.disable();
|
|
|
|
|
editor.toolbar.enable();
|
2026-04-29 15:48:36 -07:00
|
|
|
const bold = toolbar.querySelector('.ribbit-btn-bold');
|
2026-04-28 23:08:20 -07:00
|
|
|
expect(bold?.classList.contains('disabled')).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('updateActiveState', () => {
|
|
|
|
|
it('sets active class on matching buttons', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
editor.toolbar.render();
|
|
|
|
|
editor.toolbar.updateActiveState(['bold']);
|
|
|
|
|
expect(editor.toolbar.buttons.get('bold')!.element?.classList.contains('active')).toBe(true);
|
|
|
|
|
expect(editor.toolbar.buttons.get('italic')!.element?.classList.contains('active')).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('clears active when not in list', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
editor.toolbar.render();
|
|
|
|
|
editor.toolbar.updateActiveState(['bold']);
|
|
|
|
|
editor.toolbar.updateActiveState([]);
|
|
|
|
|
expect(editor.toolbar.buttons.get('bold')!.element?.classList.contains('active')).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-29 00:22:00 -07:00
|
|
|
describe('heading and list buttons', () => {
|
|
|
|
|
it('registers h1-h6', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-29 00:22:00 -07:00
|
|
|
editor.run();
|
2026-04-29 15:48:36 -07:00
|
|
|
for (let level = 1; level <= 6; level++) {
|
|
|
|
|
const btn = editor.toolbar.buttons.get(`h${level}`);
|
2026-04-29 00:22:00 -07:00
|
|
|
expect(btn).toBeDefined();
|
2026-04-29 15:48:36 -07:00
|
|
|
expect(btn!.label).toBe(`H${level}`);
|
|
|
|
|
expect(btn!.shortcut).toBe(`Ctrl+${level}`);
|
2026-04-29 00:22:00 -07:00
|
|
|
expect(btn!.action).toBe('prefix');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('registers ul and ol', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-29 00:22:00 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('ul')!.shortcut).toBe('Ctrl+Shift+8');
|
|
|
|
|
expect(editor.toolbar.buttons.get('ol')!.shortcut).toBe('Ctrl+Shift+7');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('keyboard shortcuts', () => {
|
|
|
|
|
it('all formatting buttons have shortcuts', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-29 00:22:00 -07:00
|
|
|
editor.run();
|
|
|
|
|
const expected = ['bold', 'italic', 'code', 'link', 'save'];
|
|
|
|
|
for (const id of expected) {
|
|
|
|
|
expect(editor.toolbar.buttons.get(id)!.shortcut).toBeDefined();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('block buttons have shortcuts', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-29 00:22:00 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('fencedCode')!.shortcut).toBe('Ctrl+Shift+E');
|
|
|
|
|
expect(editor.toolbar.buttons.get('blockquote')!.shortcut).toBe('Ctrl+Shift+.');
|
|
|
|
|
expect(editor.toolbar.buttons.get('table')!.shortcut).toBe('Ctrl+Shift+T');
|
|
|
|
|
expect(editor.toolbar.buttons.get('hr')!.shortcut).toBe('Ctrl+Shift+-');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('editor actions have shortcuts', () => {
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-29 00:22:00 -07:00
|
|
|
editor.run();
|
|
|
|
|
expect(editor.toolbar.buttons.get('toggle')!.shortcut).toBe('Ctrl+Shift+V');
|
|
|
|
|
expect(editor.toolbar.buttons.get('markdown')!.shortcut).toBe('Ctrl+/');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-28 23:08:20 -07:00
|
|
|
describe('save button', () => {
|
|
|
|
|
it('triggers editor.save()', () => {
|
|
|
|
|
resetDOM();
|
|
|
|
|
let saved = false;
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({
|
2026-04-28 23:08:20 -07:00
|
|
|
autoToolbar: false,
|
2026-04-29 15:48:36 -07:00
|
|
|
on: {
|
|
|
|
|
save: () => {
|
|
|
|
|
saved = true;
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-04-28 23:08:20 -07:00
|
|
|
});
|
|
|
|
|
editor.run();
|
|
|
|
|
editor.toolbar.render();
|
|
|
|
|
editor.toolbar.buttons.get('save')!.click();
|
|
|
|
|
expect(saved).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('toggle button', () => {
|
|
|
|
|
it('switches from view to wysiwyg', () => {
|
|
|
|
|
resetDOM();
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
editor.toolbar.render();
|
|
|
|
|
expect(editor.getState()).toBe('view');
|
|
|
|
|
editor.toolbar.buttons.get('toggle')!.click();
|
|
|
|
|
expect(editor.getState()).toBe('wysiwyg');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('switches from wysiwyg to view', () => {
|
|
|
|
|
resetDOM();
|
2026-04-29 15:48:36 -07:00
|
|
|
const editor = new lib.Editor({ autoToolbar: false });
|
2026-04-28 23:08:20 -07:00
|
|
|
editor.run();
|
|
|
|
|
editor.wysiwyg();
|
|
|
|
|
editor.toolbar.render();
|
|
|
|
|
editor.toolbar.buttons.get('toggle')!.click();
|
|
|
|
|
expect(editor.getState()).toBe('view');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|