@capillarytech/creatives-library 8.0.207 → 8.0.209
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
- package/v2Containers/Whatsapp/constants.js +26 -2
- package/v2Containers/Whatsapp/index.js +4 -1
- package/v2Containers/Whatsapp/tests/index.test.js +460 -18
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Editor Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized constants for the HTML Editor component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// HTML Editor Variants
|
|
8
|
+
export const HTML_EDITOR_VARIANTS = {
|
|
9
|
+
EMAIL: 'email',
|
|
10
|
+
INAPP: 'inapp'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Device Types (for InApp variant)
|
|
14
|
+
export const DEVICE_TYPES = {
|
|
15
|
+
ANDROID: 'android',
|
|
16
|
+
IOS: 'ios'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Editor languages
|
|
20
|
+
export const EDITOR_LANGUAGES = {
|
|
21
|
+
HTML: 'html',
|
|
22
|
+
CSS: 'css',
|
|
23
|
+
JAVASCRIPT: 'javascript'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Preview modes
|
|
27
|
+
export const PREVIEW_MODES = {
|
|
28
|
+
DESKTOP: 'desktop',
|
|
29
|
+
MOBILE: 'mobile'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Device specifications for mobile preview
|
|
33
|
+
export const DEVICE_PRESETS = {
|
|
34
|
+
ANDROID_PHONE: {
|
|
35
|
+
key: 'android-phone',
|
|
36
|
+
width: 393,
|
|
37
|
+
height: 851,
|
|
38
|
+
devicePixelRatio: 2.75,
|
|
39
|
+
userAgent: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36',
|
|
40
|
+
platform: 'android',
|
|
41
|
+
model: 'Pixel 7'
|
|
42
|
+
},
|
|
43
|
+
IPHONE: {
|
|
44
|
+
key: 'iphone',
|
|
45
|
+
width: 390,
|
|
46
|
+
height: 844,
|
|
47
|
+
devicePixelRatio: 3,
|
|
48
|
+
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)',
|
|
49
|
+
platform: 'ios',
|
|
50
|
+
model: 'iPhone 15'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Validation severity levels
|
|
55
|
+
export const VALIDATION_SEVERITY = {
|
|
56
|
+
ERROR: 'error',
|
|
57
|
+
WARNING: 'warning',
|
|
58
|
+
INFO: 'info'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Validation types
|
|
62
|
+
export const VALIDATION_TYPES = {
|
|
63
|
+
HTML: 'html',
|
|
64
|
+
CSS: 'css',
|
|
65
|
+
JAVASCRIPT: 'javascript',
|
|
66
|
+
SECURITY: 'security'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Error types for preview
|
|
70
|
+
export const PREVIEW_ERROR_TYPES = {
|
|
71
|
+
RUNTIME: 'runtime',
|
|
72
|
+
RENDER: 'render',
|
|
73
|
+
SECURITY: 'security'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Performance thresholds
|
|
77
|
+
export const PERFORMANCE = {
|
|
78
|
+
PREVIEW_UPDATE_DEBOUNCE: 300, // ms
|
|
79
|
+
VALIDATION_DEBOUNCE: 500, // ms
|
|
80
|
+
AUTO_SAVE_INTERVAL: 30000, // ms
|
|
81
|
+
MAX_CONTENT_LENGTH: 100000, // characters
|
|
82
|
+
PERFORMANCE_WARNING_THRESHOLD: 50000 // characters
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Layout constraints
|
|
86
|
+
export const LAYOUT = {
|
|
87
|
+
MIN_PANE_SIZE: 20, // percentage
|
|
88
|
+
MAX_PANE_SIZE: 80, // percentage
|
|
89
|
+
DEFAULT_SPLIT_SIZES: [50, 50], // [left, right] percentages
|
|
90
|
+
GUTTER_SIZE: 8, // pixels
|
|
91
|
+
MOBILE_BREAKPOINT: 768, // pixels
|
|
92
|
+
MOBILE_WIDTH_DEFAULT: 375 // pixels
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
// CodeMirror configuration
|
|
97
|
+
export const CODEMIRROR_CONFIG = {
|
|
98
|
+
THEME: 'default',
|
|
99
|
+
FONT_SIZE: 14,
|
|
100
|
+
FONT_FAMILY: '"Fira Code", "Monaco", "Menlo", "Consolas", monospace',
|
|
101
|
+
LINE_NUMBERS: true,
|
|
102
|
+
HIGHLIGHT_ACTIVE_LINE: true,
|
|
103
|
+
BRACKET_MATCHING: true,
|
|
104
|
+
AUTO_CLOSE_BRACKETS: true,
|
|
105
|
+
FOLD_GUTTER: true,
|
|
106
|
+
SEARCH_ENABLED: true,
|
|
107
|
+
AUTOCOMPLETE_ENABLED: true,
|
|
108
|
+
LINT_ENABLED: true
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// HTML validation rules (HTMLHint)
|
|
112
|
+
export const HTML_VALIDATION_RULES = {
|
|
113
|
+
'tag-pair': true,
|
|
114
|
+
'tag-self-close': true,
|
|
115
|
+
'tagname-lowercase': true,
|
|
116
|
+
'attr-lowercase': true,
|
|
117
|
+
'attr-value-double-quotes': true,
|
|
118
|
+
'id-unique': true,
|
|
119
|
+
'src-not-empty': true,
|
|
120
|
+
'title-require': false,
|
|
121
|
+
'alt-require': true,
|
|
122
|
+
'doctype-first': false, // Optional: support both fragments and full documents
|
|
123
|
+
'doctype-html5': false, // Optional: support both fragments and full documents
|
|
124
|
+
'head-script-disabled': false,
|
|
125
|
+
'style-disabled': false,
|
|
126
|
+
'inline-style-disabled': false,
|
|
127
|
+
'inline-script-disabled': false,
|
|
128
|
+
'space-tab-mixed-disabled': 'space',
|
|
129
|
+
'spec-char-escape': true
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// CSS validation rules
|
|
133
|
+
export const CSS_VALIDATION_RULES = {
|
|
134
|
+
// Will be implemented with Stylelint or similar
|
|
135
|
+
'property-case': 'lower',
|
|
136
|
+
'value-case': 'lower',
|
|
137
|
+
'declaration-colon-space-after': 'always',
|
|
138
|
+
'declaration-colon-space-before': 'never',
|
|
139
|
+
'block-closing-brace-newline-after': 'always',
|
|
140
|
+
'block-opening-brace-space-before': 'always'
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// JavaScript validation rules (ESLint-style)
|
|
144
|
+
export const JS_VALIDATION_RULES = {
|
|
145
|
+
// Basic rules for HTML editor context
|
|
146
|
+
'no-undef': 'error',
|
|
147
|
+
'no-unused-vars': 'warn',
|
|
148
|
+
'no-console': 'warn',
|
|
149
|
+
'semi': ['error', 'always'],
|
|
150
|
+
'quotes': ['error', 'single'],
|
|
151
|
+
'indent': ['error', 2]
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Keyboard shortcuts
|
|
155
|
+
export const KEYBOARD_SHORTCUTS = {
|
|
156
|
+
SAVE: 'Ctrl+S',
|
|
157
|
+
SAVE_MAC: 'Cmd+S',
|
|
158
|
+
FULLSCREEN: 'F11',
|
|
159
|
+
TOGGLE_PREVIEW: 'Ctrl+P',
|
|
160
|
+
TOGGLE_PREVIEW_MAC: 'Cmd+P',
|
|
161
|
+
FORMAT_CODE: 'Shift+Alt+F',
|
|
162
|
+
FORMAT_CODE_MAC: 'Shift+Option+F',
|
|
163
|
+
FIND: 'Ctrl+F',
|
|
164
|
+
FIND_MAC: 'Cmd+F',
|
|
165
|
+
REPLACE: 'Ctrl+H',
|
|
166
|
+
REPLACE_MAC: 'Cmd+Option+F'
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Feature flags
|
|
170
|
+
export const FEATURES = {
|
|
171
|
+
AUTO_SAVE: true,
|
|
172
|
+
VALIDATION: true,
|
|
173
|
+
MOBILE_PREVIEW: true,
|
|
174
|
+
DEVICE_PREVIEW: true,
|
|
175
|
+
FULLSCREEN_MODE: true,
|
|
176
|
+
KEYBOARD_SHORTCUTS: true,
|
|
177
|
+
ERROR_RECOVERY: true,
|
|
178
|
+
PERFORMANCE_MONITORING: true,
|
|
179
|
+
ACCESSIBILITY_FEATURES: true
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Animation durations (ms)
|
|
183
|
+
export const ANIMATIONS = {
|
|
184
|
+
FAST: 150,
|
|
185
|
+
NORMAL: 300,
|
|
186
|
+
SLOW: 500,
|
|
187
|
+
PANEL_RESIZE: 200,
|
|
188
|
+
FADE_IN: 300,
|
|
189
|
+
SLIDE_IN: 250
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Z-index layers
|
|
193
|
+
export const Z_INDEX = {
|
|
194
|
+
BASE: 1,
|
|
195
|
+
OVERLAY: 1000,
|
|
196
|
+
MODAL: 1050,
|
|
197
|
+
FULLSCREEN: 9999,
|
|
198
|
+
TOOLTIP: 10000
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Default HTML content template
|
|
202
|
+
export const DEFAULT_HTML_CONTENT = `<!DOCTYPE html>
|
|
203
|
+
<html lang="en">
|
|
204
|
+
<head>
|
|
205
|
+
<meta charset="UTF-8">
|
|
206
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
207
|
+
<title>HTML Editor Preview</title>
|
|
208
|
+
<style>
|
|
209
|
+
/* Your CSS styles here */
|
|
210
|
+
body {
|
|
211
|
+
font-family: Arial, sans-serif;
|
|
212
|
+
margin: 0;
|
|
213
|
+
padding: 20px;
|
|
214
|
+
background-color: #f5f5f5;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.container {
|
|
218
|
+
max-width: 800px;
|
|
219
|
+
margin: 0 auto;
|
|
220
|
+
background: white;
|
|
221
|
+
padding: 20px;
|
|
222
|
+
border-radius: 8px;
|
|
223
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
224
|
+
}
|
|
225
|
+
</style>
|
|
226
|
+
</head>
|
|
227
|
+
<body>
|
|
228
|
+
<div class="container">
|
|
229
|
+
<h1>Welcome to HTML Editor</h1>
|
|
230
|
+
<p>Start editing your HTML, CSS, and JavaScript code to see the live preview here.</p>
|
|
231
|
+
<button id="demoButton">Click me!</button>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<script>
|
|
235
|
+
// Your JavaScript code here
|
|
236
|
+
document.getElementById('demoButton').addEventListener('click', function() {
|
|
237
|
+
alert('Hello from the HTML Editor!');
|
|
238
|
+
});
|
|
239
|
+
</script>
|
|
240
|
+
</body>
|
|
241
|
+
</html>`;
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEditorContent Hook Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the useEditorContent custom hook that manages unified editor content state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render, screen, act } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import useEditorContent from '../useEditorContent';
|
|
11
|
+
|
|
12
|
+
// Test wrapper component
|
|
13
|
+
const TestComponent = ({ initialContent, options, onStateChange }) => {
|
|
14
|
+
const editorState = useEditorContent(initialContent, options);
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
if (onStateChange) {
|
|
18
|
+
onStateChange(editorState);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<div data-testid="content">{editorState.content}</div>
|
|
25
|
+
<div data-testid="is-dirty">{String(editorState.isDirty)}</div>
|
|
26
|
+
<div data-testid="is-empty">{String(editorState.isEmpty)}</div>
|
|
27
|
+
<div data-testid="character-count">{editorState.characterCount}</div>
|
|
28
|
+
<div data-testid="can-save">{String(editorState.canSave)}</div>
|
|
29
|
+
<div data-testid="is-large">{String(editorState.isLargeContent)}</div>
|
|
30
|
+
<div data-testid="auto-save-enabled">{String(editorState.isAutoSaveEnabled)}</div>
|
|
31
|
+
<button onClick={() => editorState.updateContent('Updated content')} data-testid="update">
|
|
32
|
+
Update
|
|
33
|
+
</button>
|
|
34
|
+
<button onClick={() => editorState.resetContent()} data-testid="reset">
|
|
35
|
+
Reset
|
|
36
|
+
</button>
|
|
37
|
+
<button onClick={() => editorState.saveContent()} data-testid="save">
|
|
38
|
+
Save
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('useEditorContent', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
jest.useFakeTimers();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.runOnlyPendingTimers();
|
|
51
|
+
jest.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('Initial State', () => {
|
|
55
|
+
it('initializes with default content when no initial content provided', () => {
|
|
56
|
+
render(<TestComponent />);
|
|
57
|
+
|
|
58
|
+
expect(screen.getByTestId('content')).toHaveTextContent('<!DOCTYPE html>');
|
|
59
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('false');
|
|
60
|
+
expect(screen.getByTestId('is-empty')).toHaveTextContent('false');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('initializes with provided initial content', () => {
|
|
64
|
+
render(<TestComponent initialContent="<h1>Test</h1>" />);
|
|
65
|
+
|
|
66
|
+
expect(screen.getByTestId('content')).toHaveTextContent('<h1>Test</h1>');
|
|
67
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('false');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('initializes with auto-save enabled by default', () => {
|
|
71
|
+
render(<TestComponent />);
|
|
72
|
+
|
|
73
|
+
expect(screen.getByTestId('auto-save-enabled')).toHaveTextContent('true');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('respects auto-save option', () => {
|
|
77
|
+
render(<TestComponent options={{ autoSave: false }} />);
|
|
78
|
+
|
|
79
|
+
expect(screen.getByTestId('auto-save-enabled')).toHaveTextContent('false');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('Content Updates', () => {
|
|
84
|
+
it('updates content when updateContent is called', () => {
|
|
85
|
+
let editorState;
|
|
86
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
87
|
+
|
|
88
|
+
act(() => {
|
|
89
|
+
editorState.updateContent('<p>New content</p>');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(screen.getByTestId('content')).toHaveTextContent('<p>New content</p>');
|
|
93
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('true');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('marks content as dirty after update', () => {
|
|
97
|
+
let editorState;
|
|
98
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
99
|
+
|
|
100
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('false');
|
|
101
|
+
|
|
102
|
+
act(() => {
|
|
103
|
+
editorState.updateContent('<p>Modified</p>');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('true');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not update if content is the same', () => {
|
|
110
|
+
const onChange = jest.fn();
|
|
111
|
+
let editorState;
|
|
112
|
+
render(<TestComponent
|
|
113
|
+
initialContent="<p>Same</p>"
|
|
114
|
+
options={{ onChange }}
|
|
115
|
+
onStateChange={(state) => { editorState = state; }}
|
|
116
|
+
/>);
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
editorState.updateContent('<p>Same</p>');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// onChange should not be called if content hasn't changed
|
|
123
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('calls onChange callback when content updates', () => {
|
|
127
|
+
const onChange = jest.fn();
|
|
128
|
+
let editorState;
|
|
129
|
+
render(<TestComponent
|
|
130
|
+
options={{ onChange }}
|
|
131
|
+
onStateChange={(state) => { editorState = state; }}
|
|
132
|
+
/>);
|
|
133
|
+
|
|
134
|
+
act(() => {
|
|
135
|
+
editorState.updateContent('<p>New</p>');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
act(() => {
|
|
139
|
+
jest.advanceTimersByTime(300);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(onChange).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Content State', () => {
|
|
147
|
+
it('reports isEmpty correctly for empty content', () => {
|
|
148
|
+
let editorState;
|
|
149
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
150
|
+
|
|
151
|
+
// Update to empty content
|
|
152
|
+
act(() => {
|
|
153
|
+
editorState.updateContent('');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(screen.getByTestId('is-empty')).toHaveTextContent('true');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('reports isEmpty correctly for non-empty content', () => {
|
|
160
|
+
render(<TestComponent initialContent="<p>Content</p>" />);
|
|
161
|
+
|
|
162
|
+
expect(screen.getByTestId('is-empty')).toHaveTextContent('false');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('calculates character count correctly', () => {
|
|
166
|
+
const content = '<p>Test</p>';
|
|
167
|
+
render(<TestComponent initialContent={content} />);
|
|
168
|
+
|
|
169
|
+
expect(screen.getByTestId('character-count')).toHaveTextContent(String(content.length));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('detects large content', async () => {
|
|
173
|
+
// Create content larger than threshold (make it 100KB to be safe)
|
|
174
|
+
const largeContent = 'a'.repeat(100000);
|
|
175
|
+
let editorState;
|
|
176
|
+
const { rerender } = render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
177
|
+
|
|
178
|
+
await act(async () => {
|
|
179
|
+
editorState.updateContent(largeContent);
|
|
180
|
+
// Wait for state update
|
|
181
|
+
await Promise.resolve();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Force TestComponent to re-render and call onStateChange with updated state
|
|
185
|
+
rerender(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
186
|
+
|
|
187
|
+
// Now check the state
|
|
188
|
+
expect(editorState.isLargeContent).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('Reset Functionality', () => {
|
|
193
|
+
it('resets to initial content', () => {
|
|
194
|
+
const initialContent = '<h1>Initial</h1>';
|
|
195
|
+
let editorState;
|
|
196
|
+
render(<TestComponent
|
|
197
|
+
initialContent={initialContent}
|
|
198
|
+
onStateChange={(state) => { editorState = state; }}
|
|
199
|
+
/>);
|
|
200
|
+
|
|
201
|
+
act(() => {
|
|
202
|
+
editorState.updateContent('<p>Modified</p>');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(screen.getByTestId('content')).toHaveTextContent('<p>Modified</p>');
|
|
206
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('true');
|
|
207
|
+
|
|
208
|
+
act(() => {
|
|
209
|
+
editorState.resetContent();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(screen.getByTestId('content')).toHaveTextContent(initialContent);
|
|
213
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('false');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('resets to default content if no initial content', () => {
|
|
217
|
+
let editorState;
|
|
218
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
219
|
+
|
|
220
|
+
act(() => {
|
|
221
|
+
editorState.updateContent('<p>Modified</p>');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
act(() => {
|
|
225
|
+
editorState.resetContent();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(screen.getByTestId('content')).toHaveTextContent('<!DOCTYPE html>');
|
|
229
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('false');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('Save Functionality', () => {
|
|
234
|
+
it('reports canSave as false when no onSave provided', () => {
|
|
235
|
+
let editorState;
|
|
236
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
237
|
+
|
|
238
|
+
act(() => {
|
|
239
|
+
editorState.updateContent('<p>Modified</p>');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(screen.getByTestId('can-save')).toHaveTextContent('false');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('reports canSave as true when onSave provided and content is dirty', () => {
|
|
246
|
+
const onSave = jest.fn();
|
|
247
|
+
let editorState;
|
|
248
|
+
render(<TestComponent
|
|
249
|
+
options={{ onSave }}
|
|
250
|
+
onStateChange={(state) => { editorState = state; }}
|
|
251
|
+
/>);
|
|
252
|
+
|
|
253
|
+
act(() => {
|
|
254
|
+
editorState.updateContent('<p>Modified</p>');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(screen.getByTestId('can-save')).toHaveTextContent('true');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('calls onSave callback when saveContent is called', async () => {
|
|
261
|
+
const onSave = jest.fn().mockResolvedValue(true);
|
|
262
|
+
let editorState;
|
|
263
|
+
render(<TestComponent
|
|
264
|
+
options={{ onSave }}
|
|
265
|
+
onStateChange={(state) => { editorState = state; }}
|
|
266
|
+
/>);
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
editorState.updateContent('<p>Modified</p>');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await act(async () => {
|
|
273
|
+
await editorState.saveContent();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(onSave).toHaveBeenCalledWith('<p>Modified</p>');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('marks content as saved after successful save', async () => {
|
|
280
|
+
const onSave = jest.fn().mockResolvedValue(true);
|
|
281
|
+
let editorState;
|
|
282
|
+
render(<TestComponent
|
|
283
|
+
options={{ onSave }}
|
|
284
|
+
onStateChange={(state) => { editorState = state; }}
|
|
285
|
+
/>);
|
|
286
|
+
|
|
287
|
+
act(() => {
|
|
288
|
+
editorState.updateContent('<p>Modified</p>');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('true');
|
|
292
|
+
|
|
293
|
+
await act(async () => {
|
|
294
|
+
await editorState.saveContent();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(screen.getByTestId('is-dirty')).toHaveTextContent('false');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('does not save if content is not dirty', async () => {
|
|
301
|
+
const onSave = jest.fn().mockResolvedValue(true);
|
|
302
|
+
let editorState;
|
|
303
|
+
render(<TestComponent
|
|
304
|
+
options={{ onSave }}
|
|
305
|
+
onStateChange={(state) => { editorState = state; }}
|
|
306
|
+
/>);
|
|
307
|
+
|
|
308
|
+
const result = await act(async () => {
|
|
309
|
+
return await editorState.saveContent();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(result).toBe(false);
|
|
313
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('Auto-save', () => {
|
|
318
|
+
it('toggles auto-save state', () => {
|
|
319
|
+
let editorState;
|
|
320
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
321
|
+
|
|
322
|
+
expect(screen.getByTestId('auto-save-enabled')).toHaveTextContent('true');
|
|
323
|
+
|
|
324
|
+
act(() => {
|
|
325
|
+
editorState.toggleAutoSave();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(screen.getByTestId('auto-save-enabled')).toHaveTextContent('false');
|
|
329
|
+
|
|
330
|
+
act(() => {
|
|
331
|
+
editorState.toggleAutoSave();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(screen.getByTestId('auto-save-enabled')).toHaveTextContent('true');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('auto-saves after interval when enabled and dirty', async () => {
|
|
338
|
+
const onSave = jest.fn().mockResolvedValue(true);
|
|
339
|
+
let editorState;
|
|
340
|
+
render(<TestComponent
|
|
341
|
+
options={{ onSave, autoSaveInterval: 5000 }}
|
|
342
|
+
onStateChange={(state) => { editorState = state; }}
|
|
343
|
+
/>);
|
|
344
|
+
|
|
345
|
+
act(() => {
|
|
346
|
+
editorState.updateContent('<p>Modified</p>');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
350
|
+
|
|
351
|
+
await act(async () => {
|
|
352
|
+
jest.advanceTimersByTime(5000);
|
|
353
|
+
await Promise.resolve();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(onSave).toHaveBeenCalled();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('does not auto-save when auto-save is disabled', async () => {
|
|
360
|
+
const onSave = jest.fn().mockResolvedValue(true);
|
|
361
|
+
let editorState;
|
|
362
|
+
render(<TestComponent
|
|
363
|
+
options={{ onSave, autoSave: false, autoSaveInterval: 5000 }}
|
|
364
|
+
onStateChange={(state) => { editorState = state; }}
|
|
365
|
+
/>);
|
|
366
|
+
|
|
367
|
+
act(() => {
|
|
368
|
+
editorState.updateContent('<p>Modified</p>');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await act(async () => {
|
|
372
|
+
jest.advanceTimersByTime(5000);
|
|
373
|
+
await Promise.resolve();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('Validation Integration', () => {
|
|
381
|
+
it('registers and calls validation callback', () => {
|
|
382
|
+
const validationCallback = jest.fn();
|
|
383
|
+
let editorState;
|
|
384
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
385
|
+
|
|
386
|
+
act(() => {
|
|
387
|
+
editorState.registerValidationCallback(validationCallback);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
act(() => {
|
|
391
|
+
editorState.validateContent();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(validationCallback).toHaveBeenCalled();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('unregisters validation callback', () => {
|
|
398
|
+
const validationCallback = jest.fn();
|
|
399
|
+
let editorState;
|
|
400
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
401
|
+
|
|
402
|
+
act(() => {
|
|
403
|
+
editorState.registerValidationCallback(validationCallback);
|
|
404
|
+
editorState.unregisterValidationCallback();
|
|
405
|
+
editorState.validateContent();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(validationCallback).not.toHaveBeenCalled();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('calls validation callback on content update with debounce', () => {
|
|
412
|
+
const validationCallback = jest.fn();
|
|
413
|
+
let editorState;
|
|
414
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
415
|
+
|
|
416
|
+
act(() => {
|
|
417
|
+
editorState.registerValidationCallback(validationCallback);
|
|
418
|
+
editorState.updateContent('<p>New</p>');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(validationCallback).not.toHaveBeenCalled();
|
|
422
|
+
|
|
423
|
+
act(() => {
|
|
424
|
+
jest.advanceTimersByTime(500);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(validationCallback).toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('Computed Properties', () => {
|
|
432
|
+
it('provides content analysis', () => {
|
|
433
|
+
let editorState;
|
|
434
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
435
|
+
|
|
436
|
+
expect(editorState.contentAnalysis).toBeDefined();
|
|
437
|
+
expect(editorState.contentAnalysis).toHaveProperty('size');
|
|
438
|
+
expect(editorState.contentAnalysis).toHaveProperty('isLarge');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('provides performance warning flag', () => {
|
|
442
|
+
let editorState;
|
|
443
|
+
render(<TestComponent onStateChange={(state) => { editorState = state; }} />);
|
|
444
|
+
|
|
445
|
+
expect(editorState.shouldShowPerformanceWarning).toBeDefined();
|
|
446
|
+
expect(typeof editorState.shouldShowPerformanceWarning).toBe('boolean');
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|