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
JavaScript
/**
* 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
}
},
};