UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

585 lines (584 loc) 21.3 kB
/** * Standardized browser API mocks for testing * Provides reusable mocking utilities for localStorage, sessionStorage, window.matchMedia, and other browser APIs */ import { vi } from 'vitest'; /** * Creates a mock for localStorage/sessionStorage with common methods */ export function createStorageMock() { return { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn(), key: vi.fn(), length: 0, }; } /** * Creates a mock for window.matchMedia */ export function createMatchMediaMock(matches = false) { return vi.fn().mockImplementation((query) => ({ matches, media: query, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), onchange: null, })); } /** * Creates a comprehensive set of browser API mocks */ export function createBrowserMocks() { const localStorage = createStorageMock(); const sessionStorage = createStorageMock(); const matchMedia = createMatchMediaMock(); return { localStorage, sessionStorage, matchMedia, // Add more browser APIs as needed getComputedStyle: vi.fn().mockReturnValue({}), ResizeObserver: vi.fn().mockImplementation((callback) => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), // Store the callback for potential use in tests _callback: callback, })), IntersectionObserver: vi.fn().mockImplementation((callback) => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), // Store the callback for potential use in tests _callback: callback, })), }; } /** * Safely defines or redefines a property on an object */ function safeDefineProperty(obj, prop, value) { try { // Check if property exists and is configurable const descriptor = Object.getOwnPropertyDescriptor(obj, prop); if (descriptor && !descriptor.configurable) { // If not configurable, try to assign directly obj[prop] = value; } else { // Otherwise use defineProperty Object.defineProperty(obj, prop, { value, writable: true, configurable: true, }); } } catch (error) { // Fallback to direct assignment try { obj[prop] = value; } catch (e) { // If all else fails, log and continue console.warn(`Failed to mock ${prop}:`, e); } } } /** * Sets up browser API mocks on the global window object */ export function setupBrowserMocks() { const mocks = createBrowserMocks(); // Setup all browser API mocks safely safeDefineProperty(window, 'localStorage', mocks.localStorage); safeDefineProperty(window, 'sessionStorage', mocks.sessionStorage); safeDefineProperty(window, 'matchMedia', mocks.matchMedia); safeDefineProperty(window, 'getComputedStyle', mocks.getComputedStyle); safeDefineProperty(window, 'ResizeObserver', mocks.ResizeObserver); safeDefineProperty(window, 'IntersectionObserver', mocks.IntersectionObserver); return mocks; } /** * Clears all browser API mocks */ export function clearBrowserMocks() { vi.restoreAllMocks(); // For properties that might not be configurable, set them to undefined instead const properties = [ 'localStorage', 'sessionStorage', 'matchMedia', 'getComputedStyle', 'ResizeObserver', 'IntersectionObserver', ]; properties.forEach(prop => { try { // Try to delete first delete window[prop]; } catch (e) { // If deletion fails, try to redefine as undefined try { Object.defineProperty(window, prop, { value: undefined, writable: true, configurable: true }); } catch (e2) { // If that also fails, just ignore } } }); } /** * Theme-specific mocking utilities */ export const themeMocks = { /** * Sets up theme-related mocks for light mode */ setupLightTheme() { const mocks = setupBrowserMocks(); mocks.localStorage.getItem.mockReturnValue('light'); mocks.matchMedia.mockImplementation((query) => ({ matches: query === '(prefers-color-scheme: light)', media: query, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), onchange: null, })); return mocks; }, /** * Sets up theme-related mocks for dark mode */ setupDarkTheme() { const mocks = setupBrowserMocks(); mocks.localStorage.getItem.mockReturnValue('dark'); mocks.matchMedia.mockImplementation((query) => ({ matches: query === '(prefers-color-scheme: dark)', media: query, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), onchange: null, })); return mocks; }, /** * Sets up theme-related mocks for auto mode with system preference */ setupAutoTheme(systemPrefersDark = false) { const mocks = setupBrowserMocks(); mocks.localStorage.getItem.mockReturnValue('auto'); mocks.matchMedia.mockImplementation((query) => ({ matches: systemPrefersDark && query === '(prefers-color-scheme: dark)', media: query, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), onchange: null, })); return mocks; }, }; /** * Modal-specific mocking utilities */ export const modalMocks = { /** * Sets up mocks for modal components */ setup() { const mocks = setupBrowserMocks(); // Mock focus management const mockFocus = vi.fn(); const mockBlur = vi.fn(); // Mock document.activeElement Object.defineProperty(document, 'activeElement', { value: { focus: mockFocus, blur: mockBlur }, writable: true, }); // Mock document.body if (!document.body) { document.body = document.createElement('body'); } return { ...mocks, focus: mockFocus, blur: mockBlur }; }, }; /** * Tooltip-specific mocking utilities */ export const tooltipMocks = { /** * Sets up mocks for tooltip components */ setup() { const mocks = setupBrowserMocks(); // Mock getBoundingClientRect const mockGetBoundingClientRect = vi.fn().mockReturnValue({ x: 0, y: 0, width: 100, height: 50, top: 0, right: 100, bottom: 50, left: 0, toJSON: vi.fn(), }); // Mock element methods Element.prototype.getBoundingClientRect = mockGetBoundingClientRect; return { ...mocks, getBoundingClientRect: mockGetBoundingClientRect }; }, }; /** * TipTap/ProseMirror-specific mocking utilities for editor components */ export const editorMocks = { /** * Sets up comprehensive mocks for TipTap/ProseMirror editor functionality */ setup() { const mocks = setupBrowserMocks(); // Mock DOM selection APIs that ProseMirror requires if (!window.getSelection) { window.getSelection = vi.fn(() => { const mockRange = { startContainer: document.body, startOffset: 0, endContainer: document.body, endOffset: 0, collapsed: true, commonAncestorContainer: document.body, cloneContents: vi.fn(() => document.createDocumentFragment()), cloneRange: vi.fn(), deleteContents: vi.fn(), insertNode: vi.fn(), selectNode: vi.fn(), selectNodeContents: vi.fn(), setStart: vi.fn(), setEnd: vi.fn(), toString: vi.fn(() => ""), compareBoundaryPoints: vi.fn(() => 0), createContextualFragment: vi.fn(() => document.createDocumentFragment()), extractContents: vi.fn(() => document.createDocumentFragment()), getBoundingClientRect: vi.fn(() => ({ x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0, toJSON: vi.fn() })), getClientRects: vi.fn(() => []) }; return { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0, isCollapsed: true, rangeCount: 0, type: "None", addRange: vi.fn(), removeAllRanges: vi.fn(), collapseToStart: vi.fn(), collapseToEnd: vi.fn(), collapse: vi.fn(), extend: vi.fn(), selectAllChildren: vi.fn(), setBaseAndExtent: vi.fn(), setPosition: vi.fn(), toString: vi.fn(() => ""), getRangeAt: vi.fn(() => mockRange), containsNode: vi.fn(() => false), modify: vi.fn(), deleteFromDocument: vi.fn(), empty: vi.fn() }; }); } // Mock document.createRange if (!document.createRange) { document.createRange = vi.fn(() => ({ startContainer: document.body, startOffset: 0, endContainer: document.body, endOffset: 0, collapsed: true, commonAncestorContainer: document.body, cloneContents: vi.fn(() => document.createDocumentFragment()), cloneRange: vi.fn(), deleteContents: vi.fn(), insertNode: vi.fn(), selectNode: vi.fn(), selectNodeContents: vi.fn(), setStart: vi.fn(), setEnd: vi.fn(), setStartBefore: vi.fn(), setStartAfter: vi.fn(), setEndBefore: vi.fn(), setEndAfter: vi.fn(), toString: vi.fn(() => ""), compareBoundaryPoints: vi.fn(() => 0), createContextualFragment: vi.fn(() => document.createDocumentFragment()), extractContents: vi.fn(() => document.createDocumentFragment()), getBoundingClientRect: vi.fn(() => ({ x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0, toJSON: vi.fn() })), getClientRects: vi.fn(() => []), intersectsNode: vi.fn(() => false), isPointInRange: vi.fn(() => false), comparePoint: vi.fn(() => 0), surroundContents: vi.fn(), detach: vi.fn() })); } // Enhanced Element.prototype methods for ProseMirror Element.prototype.getClientRects = vi.fn(() => [{ x: 0, y: 0, width: 100, height: 20, top: 0, right: 100, bottom: 20, left: 0, toJSON: vi.fn() }]); Element.prototype.getBoundingClientRect = vi.fn(() => ({ x: 0, y: 0, width: 100, height: 20, top: 0, right: 100, bottom: 20, left: 0, toJSON: vi.fn() })); Element.prototype.scrollIntoView = vi.fn(); Element.prototype.scrollTo = vi.fn(); Element.prototype.scroll = vi.fn(); // Mock contenteditable behavior for ProseMirror Object.defineProperty(HTMLElement.prototype, "isContentEditable", { get: vi.fn(() => false), configurable: true }); Object.defineProperty(HTMLElement.prototype, "contentEditable", { get: vi.fn(() => "false"), set: vi.fn(), configurable: true }); // Mock execCommand for legacy browser editing if (!document.execCommand) { document.execCommand = vi.fn(() => true); } // Mock queryCommandState/queryCommandValue for ProseMirror if (!document.queryCommandState) { document.queryCommandState = vi.fn(() => false); } if (!document.queryCommandValue) { document.queryCommandValue = vi.fn(() => ""); } if (!document.queryCommandSupported) { document.queryCommandSupported = vi.fn(() => true); } // Mock DOMParser for HTML parsing in ProseMirror if (!window.DOMParser) { window.DOMParser = vi.fn().mockImplementation(() => ({ parseFromString: vi.fn(() => { const doc = document.implementation.createHTMLDocument(); doc.body.innerHTML = '<p></p>'; return doc; }), parseFromBuffer: vi.fn(() => document.implementation.createHTMLDocument()), parseFromStream: vi.fn(() => document.implementation.createHTMLDocument()) })); } // Mock XMLSerializer for serializing DOM to strings if (!window.XMLSerializer) { window.XMLSerializer = vi.fn().mockImplementation(() => ({ serializeToString: vi.fn((node) => { if (node.outerHTML) return node.outerHTML; if (node.textContent !== undefined) return node.textContent; return ""; }) })); } // Enhanced MutationObserver for ProseMirror DOM observation if (!window.MutationObserver) { safeDefineProperty(window, 'MutationObserver', vi.fn().mockImplementation((callback) => { const observer = { observe: vi.fn(), disconnect: vi.fn(), takeRecords: vi.fn(() => []), _callback: callback, _observing: [], // Add methods that ProseMirror might expect _trigger: vi.fn((mutations = []) => { if (callback && typeof callback === 'function') { callback(mutations, observer); } }) }; return observer; })); } // Mock clipboard API for copy/paste operations if (!navigator.clipboard) { Object.defineProperty(navigator, 'clipboard', { value: { writeText: vi.fn(() => Promise.resolve()), readText: vi.fn(() => Promise.resolve("")), write: vi.fn(() => Promise.resolve()), read: vi.fn(() => Promise.resolve([])) }, configurable: true }); } // Mock drag and drop for editor if (!window.DataTransfer) { safeDefineProperty(window, 'DataTransfer', vi.fn().mockImplementation(() => ({ dropEffect: 'none', effectAllowed: 'uninitialized', files: [], items: { length: 0, add: vi.fn(), remove: vi.fn(), clear: vi.fn() }, types: [], clearData: vi.fn(), getData: vi.fn(() => ""), setData: vi.fn(), setDragImage: vi.fn() }))); } // Mock focus/blur events with proper event dispatching const originalFocus = HTMLElement.prototype.focus; const originalBlur = HTMLElement.prototype.blur; HTMLElement.prototype.focus = vi.fn(function (options) { // Update document.activeElement Object.defineProperty(document, 'activeElement', { value: this, writable: true, configurable: true }); // Dispatch focus event const focusEvent = new Event('focus', { bubbles: false }); this.dispatchEvent(focusEvent); // Dispatch focusin event (bubbles) const focusinEvent = new Event('focusin', { bubbles: true }); this.dispatchEvent(focusinEvent); }); HTMLElement.prototype.blur = vi.fn(function () { // Update document.activeElement Object.defineProperty(document, 'activeElement', { value: document.body, writable: true, configurable: true }); // Dispatch blur event const blurEvent = new Event('blur', { bubbles: false }); this.dispatchEvent(blurEvent); // Dispatch focusout event (bubbles) const focusoutEvent = new Event('focusout', { bubbles: true }); this.dispatchEvent(focusoutEvent); }); // Mock input events for text editing HTMLElement.prototype.dispatchEvent = vi.fn(function (event) { // Call original dispatchEvent if available const result = Event.prototype.dispatchEvent?.call?.(this, event); // Trigger event listeners manually for testing const listeners = this._eventListeners?.[event.type]; if (listeners) { listeners.forEach((listener) => { if (typeof listener === 'function') { listener.call(this, event); } else if (listener && typeof listener.handleEvent === 'function') { listener.handleEvent(event); } }); } return result !== false; }); return { ...mocks, // Cleanup function for editor-specific mocks cleanup: () => { // Restore original methods if they existed if (originalFocus && typeof originalFocus === 'function') { HTMLElement.prototype.focus = originalFocus; } if (originalBlur && typeof originalBlur === 'function') { HTMLElement.prototype.blur = originalBlur; } } }; }, /** * Creates a mock editor element suitable for TipTap initialization */ createEditorElement() { const element = document.createElement('div'); element.contentEditable = 'false'; element.style.minHeight = '20px'; // Mock ProseMirror-specific methods element.pm = { flush: vi.fn(), destroyPlugins: vi.fn() }; return element; }, /** * Creates mock editor configuration for testing */ createEditorConfig(overrides = {}) { 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 }; } }; /** * Test utilities for component state management */ export const componentMocks = { /** * Cleans up theme manager and other global component state */ cleanupThemeManager() { // Clean up DOM theme classes document.documentElement.classList.remove('ck-theme-light', 'ck-theme-dark'); document.documentElement.removeAttribute('data-theme'); // Reset global theme manager (accessing private field for testing) try { const themeModule = require('../../../utils/theme'); if (themeModule && themeModule.getThemeManager) { const themeManager = themeModule.getThemeManager(); if (themeManager && typeof themeManager.destroy === 'function') { themeManager.destroy(); } // Force reset of global instance themeModule.globalThemeManager = null; } } catch (e) { // Ignore if theme module is not available } }, };