claritykit-svelte
Version:
A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility
382 lines (381 loc) • 11.9 kB
JavaScript
/**
* Editor test utilities for TipTap/ProseMirror components
* Provides factory functions and test helpers for editor testing
*/
import { vi } from 'vitest';
import { editorMocks } from './browser-mocks';
/**
* Mock Editor class that mimics TipTap Editor behavior
*/
export class MockEditor {
constructor(config = {}) {
Object.defineProperty(this, "element", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "extensions", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "content", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "editable", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "callbacks", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
this.element = config.element || document.createElement('div');
this.extensions = config.extensions || [];
this.content = config.content || '';
this.editable = config.editable !== false;
// Store callbacks
if (config.onTransaction)
this.callbacks.onTransaction = [config.onTransaction];
if (config.onUpdate)
this.callbacks.onUpdate = [config.onUpdate];
if (config.onFocus)
this.callbacks.onFocus = [config.onFocus];
if (config.onBlur)
this.callbacks.onBlur = [config.onBlur];
if (config.onCreate)
this.callbacks.onCreate = [config.onCreate];
if (config.onDestroy)
this.callbacks.onDestroy = [config.onDestroy];
// Initialize the mock DOM structure
this._initializeDOMStructure();
// Call onCreate if provided
if (config.onCreate) {
config.onCreate({ editor: this });
}
}
_initializeDOMStructure() {
// Create ProseMirror-like DOM structure
this.element.classList.add('ProseMirror');
this.element.contentEditable = this.editable ? 'true' : 'false';
// Add some initial content if provided
if (this.content) {
if (typeof this.content === 'string' && this.content.startsWith('<')) {
this.element.innerHTML = this.content;
}
else {
this.element.textContent = this.content;
}
}
else {
// Add empty paragraph as default
const p = document.createElement('p');
p.setAttribute('data-placeholder', 'Start typing...');
p.className = 'is-editor-empty';
this.element.appendChild(p);
}
}
// Mock editor methods
getHTML() {
return this.element.innerHTML || '<p></p>';
}
getJSON() {
return {
type: 'doc',
content: [
{
type: 'paragraph',
content: this.element.textContent ? [
{ type: 'text', text: this.element.textContent }
] : []
}
]
};
}
getText() {
return this.element.textContent || '';
}
setEditable(editable) {
this.editable = editable;
this.element.contentEditable = editable ? 'true' : 'false';
}
isActive(name) {
// Simple mock implementation
return name === 'bold' ? this.element.querySelector('strong, b') !== null :
name === 'italic' ? this.element.querySelector('em, i') !== null :
false;
}
// Mock command chain
chain() {
const self = this;
return {
focus() {
self.element.focus();
self._triggerCallback('onFocus');
return this;
},
toggleBold() {
// Mock bold toggle
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const strong = document.createElement('strong');
try {
range.surroundContents(strong);
}
catch (e) {
// Ignore errors in test environment
}
}
self._triggerCallback('onUpdate');
return this;
},
toggleItalic() {
// Mock italic toggle
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const em = document.createElement('em');
try {
range.surroundContents(em);
}
catch (e) {
// Ignore errors in test environment
}
}
self._triggerCallback('onUpdate');
return this;
},
run() {
// End of chain - trigger any pending callbacks
return true;
}
};
}
// Mock commands object
get commands() {
const self = this;
return {
setContent: (content) => {
self.content = content;
if (typeof content === 'string' && content.startsWith('<')) {
self.element.innerHTML = content;
}
else {
self.element.textContent = content;
}
self._triggerCallback('onUpdate');
return true;
},
focus: () => {
self.element.focus();
self._triggerCallback('onFocus');
return true;
},
blur: () => {
self.element.blur();
self._triggerCallback('onBlur');
return true;
}
};
}
destroy() {
this._triggerCallback('onDestroy');
// Clean up element
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
}
focus() {
this.element.focus();
this._triggerCallback('onFocus');
}
blur() {
this.element.blur();
this._triggerCallback('onBlur');
}
_triggerCallback(eventName, data) {
const callbacks = this.callbacks[eventName];
if (callbacks) {
callbacks.forEach(callback => {
try {
if (eventName === 'onUpdate') {
callback({ editor: this });
}
else if (eventName === 'onFocus' || eventName === 'onBlur') {
callback();
}
else {
callback(data || { editor: this });
}
}
catch (e) {
// Ignore callback errors in tests
}
});
}
}
// Simulate user typing
simulateTyping(text) {
this.element.textContent = text;
this.content = text;
this._triggerCallback('onUpdate');
this._triggerCallback('onTransaction');
}
// Simulate user focus
simulateFocus() {
this.element.focus();
this._triggerCallback('onFocus');
}
// Simulate user blur
simulateBlur() {
this.element.blur();
this._triggerCallback('onBlur');
}
}
/**
* Editor factory for creating consistent test setups
*/
export class EditorTestFactory {
/**
* Sets up the test environment for editor testing
*/
static setup() {
if (!this.mocks) {
this.mocks = editorMocks.setup();
}
return this.mocks;
}
/**
* Creates a mock editor instance
*/
static createEditor(config = {}) {
this.setup();
return new MockEditor(config);
}
/**
* Creates a mock editor element ready for mounting
*/
static createEditorElement() {
this.setup();
return editorMocks.createEditorElement();
}
/**
* Creates mock editor configuration
*/
static createEditorConfig(overrides = {}) {
this.setup();
return {
element: this.createEditorElement(),
extensions: [],
content: '',
editable: true,
onTransaction: vi.fn(),
onUpdate: vi.fn(),
onFocus: vi.fn(),
onBlur: vi.fn(),
onCreate: vi.fn(),
onDestroy: vi.fn(),
...overrides
};
}
/**
* Mock the TipTap Editor constructor
*/
static mockTipTapEditor() {
this.setup();
// Mock the Editor import
return vi.fn().mockImplementation((config) => {
return this.createEditor(config);
});
}
/**
* Creates a test helper for editor components
*/
static createTestHelper() {
const mocks = this.setup();
return {
/**
* Simulates text input in an editor
*/
simulateTextInput: (element, text) => {
element.textContent = text;
// Dispatch input event
const inputEvent = new Event('input', { bubbles: true });
element.dispatchEvent(inputEvent);
},
/**
* Simulates a key press in an editor
*/
simulateKeyPress: (element, key, options = {}) => {
const keyEvent = new KeyboardEvent('keydown', {
key,
code: key,
bubbles: true,
...options
});
element.dispatchEvent(keyEvent);
},
/**
* Simulates focus on an editor element
*/
simulateFocus: (element) => {
element.focus();
const focusEvent = new Event('focus', { bubbles: false });
element.dispatchEvent(focusEvent);
},
/**
* Simulates blur on an editor element
*/
simulateBlur: (element) => {
element.blur();
const blurEvent = new Event('blur', { bubbles: false });
element.dispatchEvent(blurEvent);
},
/**
* Waits for editor to be ready
*/
waitForEditor: async () => {
// Simple delay to allow editor initialization
await new Promise(resolve => setTimeout(resolve, 10));
},
/**
* Cleans up editor mocks
*/
cleanup: () => {
if (mocks.cleanup) {
mocks.cleanup();
}
this.mocks = null;
}
};
}
/**
* Cleans up all editor mocks
*/
static cleanup() {
if (this.mocks && this.mocks.cleanup) {
this.mocks.cleanup();
}
this.mocks = null;
}
}
Object.defineProperty(EditorTestFactory, "mocks", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
/**
* Default export for convenience
*/
export const editorTestUtils = EditorTestFactory;