meno-core 1.0.0
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/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- package/vite.config.ts +43 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side Form Handler
|
|
3
|
+
* Handles form submissions via fetch for forms with data-submit-handler="fetch"
|
|
4
|
+
*
|
|
5
|
+
* This script is designed to be included inline in SSR output.
|
|
6
|
+
* It's a self-executing module that attaches to forms automatically.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const formHandlerScript = `
|
|
10
|
+
(function() {
|
|
11
|
+
// Find all forms with fetch handler
|
|
12
|
+
const forms = document.querySelectorAll('form[data-submit-handler="fetch"]');
|
|
13
|
+
|
|
14
|
+
forms.forEach(function(form) {
|
|
15
|
+
form.addEventListener('submit', async function(e) {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
|
|
18
|
+
const action = form.getAttribute('action');
|
|
19
|
+
const method = form.getAttribute('method') || 'POST';
|
|
20
|
+
const successMessage = form.getAttribute('data-success-message') || 'Form submitted successfully!';
|
|
21
|
+
const errorMessage = form.getAttribute('data-error-message') || 'Something went wrong. Please try again.';
|
|
22
|
+
|
|
23
|
+
// Find or create message element
|
|
24
|
+
let messageEl = form.querySelector('[data-form-message]');
|
|
25
|
+
if (!messageEl) {
|
|
26
|
+
messageEl = document.createElement('div');
|
|
27
|
+
messageEl.setAttribute('data-form-message', 'true');
|
|
28
|
+
form.appendChild(messageEl);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find submit button and disable it
|
|
32
|
+
const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
|
|
33
|
+
const originalBtnText = submitBtn ? submitBtn.textContent || submitBtn.value : '';
|
|
34
|
+
|
|
35
|
+
if (submitBtn) {
|
|
36
|
+
submitBtn.disabled = true;
|
|
37
|
+
if (submitBtn.tagName === 'BUTTON') {
|
|
38
|
+
submitBtn.textContent = 'Sending...';
|
|
39
|
+
} else {
|
|
40
|
+
submitBtn.value = 'Sending...';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Hide any previous message
|
|
45
|
+
messageEl.style.display = 'none';
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const formData = new FormData(form);
|
|
49
|
+
|
|
50
|
+
const response = await fetch(action, {
|
|
51
|
+
method: method.toUpperCase(),
|
|
52
|
+
body: formData,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
|
|
57
|
+
if (response.ok && data.success) {
|
|
58
|
+
// Success
|
|
59
|
+
messageEl.textContent = data.message || successMessage;
|
|
60
|
+
messageEl.style.display = 'block';
|
|
61
|
+
messageEl.style.backgroundColor = '#d4edda';
|
|
62
|
+
messageEl.style.color = '#155724';
|
|
63
|
+
messageEl.style.border = '1px solid #c3e6cb';
|
|
64
|
+
|
|
65
|
+
// Optionally reset form
|
|
66
|
+
form.reset();
|
|
67
|
+
} else {
|
|
68
|
+
// Error from server
|
|
69
|
+
messageEl.textContent = data.error || errorMessage;
|
|
70
|
+
messageEl.style.display = 'block';
|
|
71
|
+
messageEl.style.backgroundColor = '#f8d7da';
|
|
72
|
+
messageEl.style.color = '#721c24';
|
|
73
|
+
messageEl.style.border = '1px solid #f5c6cb';
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Network or other error
|
|
77
|
+
messageEl.textContent = errorMessage;
|
|
78
|
+
messageEl.style.display = 'block';
|
|
79
|
+
messageEl.style.backgroundColor = '#f8d7da';
|
|
80
|
+
messageEl.style.color = '#721c24';
|
|
81
|
+
messageEl.style.border = '1px solid #f5c6cb';
|
|
82
|
+
} finally {
|
|
83
|
+
// Re-enable submit button
|
|
84
|
+
if (submitBtn) {
|
|
85
|
+
submitBtn.disabled = false;
|
|
86
|
+
if (submitBtn.tagName === 'BUTTON') {
|
|
87
|
+
submitBtn.textContent = originalBtnText;
|
|
88
|
+
} else {
|
|
89
|
+
submitBtn.value = originalBtnText;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
})();
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if page HTML contains forms that need the fetch handler
|
|
100
|
+
*/
|
|
101
|
+
export function needsFormHandler(html: string): boolean {
|
|
102
|
+
return html.includes('data-submit-handler="fetch"');
|
|
103
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { mergeStyles } from './styleProcessor';
|
|
3
|
+
import type { StyleObject } from '../shared/types';
|
|
4
|
+
|
|
5
|
+
describe('styleProcessor', () => {
|
|
6
|
+
describe('mergeStyles', () => {
|
|
7
|
+
test('merges multiple style objects', () => {
|
|
8
|
+
const style1: StyleObject = { color: 'red', fontSize: '16px' };
|
|
9
|
+
const style2: StyleObject = { padding: '10px' };
|
|
10
|
+
const result = mergeStyles(style1, style2);
|
|
11
|
+
expect(result).toEqual({
|
|
12
|
+
color: 'red',
|
|
13
|
+
fontSize: '16px',
|
|
14
|
+
padding: '10px'
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('later styles override earlier ones', () => {
|
|
19
|
+
const style1: StyleObject = { color: 'red', fontSize: '16px' };
|
|
20
|
+
const style2: StyleObject = { color: 'blue', padding: '10px' };
|
|
21
|
+
const result = mergeStyles(style1, style2);
|
|
22
|
+
expect(result).toEqual({
|
|
23
|
+
color: 'blue',
|
|
24
|
+
fontSize: '16px',
|
|
25
|
+
padding: '10px'
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('handles undefined styles', () => {
|
|
30
|
+
const style1: StyleObject = { color: 'red' };
|
|
31
|
+
const result = mergeStyles(style1, undefined, { fontSize: '16px' });
|
|
32
|
+
expect(result).toEqual({
|
|
33
|
+
color: 'red',
|
|
34
|
+
fontSize: '16px'
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('returns empty object when no styles provided', () => {
|
|
39
|
+
const result = mergeStyles();
|
|
40
|
+
expect(result).toEqual({});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('handles all undefined styles', () => {
|
|
44
|
+
const result = mergeStyles(undefined, undefined, undefined);
|
|
45
|
+
expect(result).toEqual({});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('merges complex style properties', () => {
|
|
49
|
+
const style1: StyleObject = {
|
|
50
|
+
display: 'flex',
|
|
51
|
+
flexDirection: 'column',
|
|
52
|
+
padding: '20px'
|
|
53
|
+
};
|
|
54
|
+
const style2: StyleObject = {
|
|
55
|
+
backgroundColor: '#f0f0f0',
|
|
56
|
+
border: '1px solid #ccc'
|
|
57
|
+
};
|
|
58
|
+
const style3: StyleObject = {
|
|
59
|
+
padding: '30px',
|
|
60
|
+
margin: '10px'
|
|
61
|
+
};
|
|
62
|
+
const result = mergeStyles(style1, style2, style3);
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
display: 'flex',
|
|
65
|
+
flexDirection: 'column',
|
|
66
|
+
padding: '30px',
|
|
67
|
+
backgroundColor: '#f0f0f0',
|
|
68
|
+
border: '1px solid #ccc',
|
|
69
|
+
margin: '10px'
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('handles single style object', () => {
|
|
74
|
+
const style: StyleObject = { color: 'blue' };
|
|
75
|
+
const result = mergeStyles(style);
|
|
76
|
+
expect(result).toEqual({ color: 'blue' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('does not mutate input styles', () => {
|
|
80
|
+
const style1: StyleObject = { color: 'red' };
|
|
81
|
+
const style2: StyleObject = { fontSize: '16px' };
|
|
82
|
+
const original1 = { ...style1 };
|
|
83
|
+
const original2 = { ...style2 };
|
|
84
|
+
|
|
85
|
+
mergeStyles(style1, style2);
|
|
86
|
+
|
|
87
|
+
expect(style1).toEqual(original1);
|
|
88
|
+
expect(style2).toEqual(original2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('handles empty style objects', () => {
|
|
92
|
+
const result = mergeStyles({}, {}, {});
|
|
93
|
+
expect(result).toEqual({});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('merges with mix of empty and non-empty styles', () => {
|
|
97
|
+
const style1: StyleObject = {};
|
|
98
|
+
const style2: StyleObject = { color: 'red' };
|
|
99
|
+
const style3: StyleObject = {};
|
|
100
|
+
const result = mergeStyles(style1, style2, style3);
|
|
101
|
+
expect(result).toEqual({ color: 'red' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('handles numeric values', () => {
|
|
105
|
+
const style1: StyleObject = { opacity: 0.5, zIndex: 10 };
|
|
106
|
+
const style2: StyleObject = { zIndex: 20, fontSize: 16 };
|
|
107
|
+
const result = mergeStyles(style1, style2);
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
opacity: 0.5,
|
|
110
|
+
zIndex: 20,
|
|
111
|
+
fontSize: 16
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('preserves falsy values', () => {
|
|
116
|
+
const style1: StyleObject = { opacity: 0, margin: '0' };
|
|
117
|
+
const style2: StyleObject = { padding: '0px' };
|
|
118
|
+
const result = mergeStyles(style1, style2);
|
|
119
|
+
expect(result).toEqual({
|
|
120
|
+
opacity: 0,
|
|
121
|
+
margin: '0',
|
|
122
|
+
padding: '0px'
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Processor
|
|
3
|
+
* Handles component style merging from structured component definitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentDefinition, StyleObject, StyleValue } from '../shared/types';
|
|
7
|
+
import { processStructure, normalizeStyle } from './templateEngine';
|
|
8
|
+
|
|
9
|
+
type ComponentProps = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Merge component styles from structured component definition
|
|
13
|
+
* Returns merged props with all applicable styles
|
|
14
|
+
*/
|
|
15
|
+
export function mergeComponentStyles(
|
|
16
|
+
componentDef: ComponentDefinition,
|
|
17
|
+
props: ComponentProps
|
|
18
|
+
): ComponentProps {
|
|
19
|
+
try {
|
|
20
|
+
// Start with base props from component definition
|
|
21
|
+
let mergedProps: ComponentProps = { ...(componentDef.props || {}) };
|
|
22
|
+
|
|
23
|
+
const structuredComponentDef = componentDef.component;
|
|
24
|
+
if (!structuredComponentDef) {
|
|
25
|
+
return props || {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Build default props from structured component prop definitions (from interface)
|
|
29
|
+
const defaultProps: ComponentProps = {};
|
|
30
|
+
if (structuredComponentDef.interface) {
|
|
31
|
+
for (const [key, def] of Object.entries(structuredComponentDef.interface)) {
|
|
32
|
+
if (def && typeof def === 'object' && 'default' in def) {
|
|
33
|
+
defaultProps[key] = def.default;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const effectiveInputProps = { ...defaultProps, ...(props || {}) };
|
|
38
|
+
|
|
39
|
+
// Merge resolved props (with defaults) into mergedProps so they're available in PropsPanel
|
|
40
|
+
mergedProps = { ...mergedProps, ...effectiveInputProps };
|
|
41
|
+
|
|
42
|
+
// Compute base style from structure with templates evaluated
|
|
43
|
+
// processStructure now reads from structure.style and merges into props.style
|
|
44
|
+
try {
|
|
45
|
+
if (structuredComponentDef.structure) {
|
|
46
|
+
const processed = processStructure(structuredComponentDef.structure, { props: effectiveInputProps, componentDef: structuredComponentDef });
|
|
47
|
+
// Type guard: ensure processed is a ComponentNode
|
|
48
|
+
if (processed && typeof processed === 'object' && !Array.isArray(processed) && 'props' in processed) {
|
|
49
|
+
const structureStyle = (processed.props?.style && typeof processed.props.style === 'object' && processed.props.style !== null && !Array.isArray(processed.props.style))
|
|
50
|
+
? processed.props.style as Record<string, unknown>
|
|
51
|
+
: {};
|
|
52
|
+
const currentStyle = typeof mergedProps.style === 'object' && mergedProps.style !== null && !Array.isArray(mergedProps.style)
|
|
53
|
+
? { ...mergedProps.style as Record<string, unknown> }
|
|
54
|
+
: {};
|
|
55
|
+
mergedProps.style = { ...currentStyle, ...structureStyle };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Merge with instance props (allows overrides)
|
|
62
|
+
// Normalize responsive styles to flat StyleObject
|
|
63
|
+
const restProps = props || {};
|
|
64
|
+
const normalizedMergedStyle = normalizeStyle(mergedProps.style as StyleValue) || {};
|
|
65
|
+
const normalizedRestStyle = normalizeStyle(restProps?.style as StyleValue) || {};
|
|
66
|
+
|
|
67
|
+
mergedProps = {
|
|
68
|
+
...mergedProps,
|
|
69
|
+
...restProps,
|
|
70
|
+
style: {
|
|
71
|
+
...normalizedMergedStyle,
|
|
72
|
+
...normalizedRestStyle,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return mergedProps;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Return props as-is if merging fails
|
|
79
|
+
return props || {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Merge styles from multiple sources
|
|
85
|
+
*/
|
|
86
|
+
export function mergeStyles(...styles: (StyleObject | undefined)[]): StyleObject {
|
|
87
|
+
return styles.reduce<StyleObject>((acc, style) => {
|
|
88
|
+
if (!style) return acc;
|
|
89
|
+
return { ...acc, ...style };
|
|
90
|
+
}, {});
|
|
91
|
+
}
|
|
92
|
+
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
+
import { StyleInjector } from "./StyleInjector";
|
|
3
|
+
import { ComponentRegistry } from "../componentRegistry";
|
|
4
|
+
import { ElementRegistry } from "../elementRegistry";
|
|
5
|
+
import type { ComponentDefinition } from "../../shared/types";
|
|
6
|
+
import {
|
|
7
|
+
createTypedMockHTMLElement,
|
|
8
|
+
createTypedMockDocument,
|
|
9
|
+
type TypedMockDocument,
|
|
10
|
+
type TypedMockHTMLElement,
|
|
11
|
+
} from "../../test-utils/factories/DomMockFactory";
|
|
12
|
+
|
|
13
|
+
describe("StyleInjector", () => {
|
|
14
|
+
let componentRegistry: ComponentRegistry;
|
|
15
|
+
let elementRegistry: ElementRegistry;
|
|
16
|
+
let styleInjector: StyleInjector;
|
|
17
|
+
let mockDocument: TypedMockDocument;
|
|
18
|
+
let mockStyleTag: TypedMockHTMLElement;
|
|
19
|
+
let originalDocument: typeof document;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Save original document
|
|
23
|
+
originalDocument = global.document;
|
|
24
|
+
|
|
25
|
+
componentRegistry = new ComponentRegistry();
|
|
26
|
+
elementRegistry = new ElementRegistry();
|
|
27
|
+
styleInjector = new StyleInjector({
|
|
28
|
+
componentRegistry,
|
|
29
|
+
elementRegistry,
|
|
30
|
+
styleId: "component-css",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Create typed mock style tag
|
|
34
|
+
mockStyleTag = createTypedMockHTMLElement({
|
|
35
|
+
tagName: "STYLE",
|
|
36
|
+
id: "component-css",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Create typed mock document
|
|
40
|
+
mockDocument = createTypedMockDocument();
|
|
41
|
+
|
|
42
|
+
// Configure createElement to return our mock style tag
|
|
43
|
+
mockDocument.createElement.mockImplementation((tag: string) => {
|
|
44
|
+
if (tag === "style") {
|
|
45
|
+
return mockStyleTag;
|
|
46
|
+
}
|
|
47
|
+
return createTypedMockHTMLElement({ tagName: tag });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Setup global document with typed mock
|
|
51
|
+
global.document = mockDocument as unknown as Document;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
// Clean up any style tags
|
|
56
|
+
styleInjector.clear();
|
|
57
|
+
// Restore original document
|
|
58
|
+
global.document = originalDocument;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("constructor", () => {
|
|
62
|
+
test("should create instance with default styleId", () => {
|
|
63
|
+
const injector = new StyleInjector({
|
|
64
|
+
componentRegistry,
|
|
65
|
+
elementRegistry,
|
|
66
|
+
});
|
|
67
|
+
expect(injector).toBeInstanceOf(StyleInjector);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("should create instance with custom styleId", () => {
|
|
71
|
+
const injector = new StyleInjector({
|
|
72
|
+
componentRegistry,
|
|
73
|
+
elementRegistry,
|
|
74
|
+
styleId: "custom-style-id",
|
|
75
|
+
});
|
|
76
|
+
expect(injector).toBeInstanceOf(StyleInjector);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("inject", () => {
|
|
81
|
+
test("should inject CSS without templates", () => {
|
|
82
|
+
const componentDef: ComponentDefinition = {
|
|
83
|
+
type: "component",
|
|
84
|
+
component: {
|
|
85
|
+
interface: {},
|
|
86
|
+
structure: { type: "node", tag: "div" },
|
|
87
|
+
css: ".test { color: red; }",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
componentRegistry.register("TestComponent", componentDef);
|
|
92
|
+
|
|
93
|
+
styleInjector.inject();
|
|
94
|
+
|
|
95
|
+
expect(mockDocument.createElement).toHaveBeenCalledWith("style");
|
|
96
|
+
expect(mockDocument.head.appendChild).toHaveBeenCalled();
|
|
97
|
+
expect(mockStyleTag.textContent).toContain(".test { color: red; }");
|
|
98
|
+
expect(mockStyleTag.textContent).toContain("Component: TestComponent");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Template-based tests require full ElementRegistry from @meno/studio
|
|
102
|
+
// These are integration tests that should be run in the studio package
|
|
103
|
+
test.skip("should inject CSS with templates per instance (requires @meno/studio ElementRegistry)", () => {
|
|
104
|
+
const componentDef: ComponentDefinition = {
|
|
105
|
+
type: "component",
|
|
106
|
+
component: {
|
|
107
|
+
interface: {},
|
|
108
|
+
structure: { type: "node", tag: "div" },
|
|
109
|
+
css: ".test { color: {{color}}; }",
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
componentRegistry.register("TestComponent", componentDef);
|
|
114
|
+
|
|
115
|
+
// Register component instances with props using typed mocks
|
|
116
|
+
const mockElement1 = createTypedMockHTMLElement({ tagName: "DIV" });
|
|
117
|
+
const mockElement2 = createTypedMockHTMLElement({ tagName: "DIV" });
|
|
118
|
+
elementRegistry.register([0], mockElement1 as unknown as HTMLElement, "TestComponent", true, {
|
|
119
|
+
color: "red",
|
|
120
|
+
});
|
|
121
|
+
elementRegistry.register([1], mockElement2 as unknown as HTMLElement, "TestComponent", true, {
|
|
122
|
+
color: "blue",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
styleInjector.inject();
|
|
126
|
+
|
|
127
|
+
expect(mockDocument.createElement).toHaveBeenCalledWith("style");
|
|
128
|
+
expect(mockDocument.head.appendChild).toHaveBeenCalled();
|
|
129
|
+
expect(mockStyleTag.textContent).toContain("color: red");
|
|
130
|
+
expect(mockStyleTag.textContent).toContain("color: blue");
|
|
131
|
+
expect(mockStyleTag.textContent).toContain("(instance)");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test.skip("should handle multiple components with mixed template/non-template CSS (requires @meno/studio ElementRegistry)", () => {
|
|
135
|
+
const componentDef1: ComponentDefinition = {
|
|
136
|
+
type: "component",
|
|
137
|
+
component: {
|
|
138
|
+
interface: {},
|
|
139
|
+
structure: { type: "node", tag: "div" },
|
|
140
|
+
css: ".test1 { color: red; }",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const componentDef2: ComponentDefinition = {
|
|
145
|
+
type: "component",
|
|
146
|
+
component: {
|
|
147
|
+
interface: {},
|
|
148
|
+
structure: { type: "node", tag: "div" },
|
|
149
|
+
css: ".test2 { color: {{color}}; }",
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
componentRegistry.register("Component1", componentDef1);
|
|
154
|
+
componentRegistry.register("Component2", componentDef2);
|
|
155
|
+
|
|
156
|
+
const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
|
|
157
|
+
elementRegistry.register([0], mockElement as unknown as HTMLElement, "Component2", true, {
|
|
158
|
+
color: "blue",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
styleInjector.inject();
|
|
162
|
+
|
|
163
|
+
expect(mockStyleTag.textContent).toContain("Component: Component1");
|
|
164
|
+
expect(mockStyleTag.textContent).toContain("Component: Component2");
|
|
165
|
+
expect(mockStyleTag.textContent).toContain(".test1 { color: red; }");
|
|
166
|
+
expect(mockStyleTag.textContent).toContain("color: blue");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("should handle existing style tag before injecting new one", () => {
|
|
170
|
+
const existingStyle = createTypedMockHTMLElement({
|
|
171
|
+
tagName: "STYLE",
|
|
172
|
+
id: "component-css",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
mockDocument.getElementById.mockReturnValue(existingStyle);
|
|
176
|
+
|
|
177
|
+
const componentDef: ComponentDefinition = {
|
|
178
|
+
type: "component",
|
|
179
|
+
component: {
|
|
180
|
+
interface: {},
|
|
181
|
+
structure: { type: "node", tag: "div" },
|
|
182
|
+
css: ".test { color: red; }",
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
componentRegistry.register("TestComponent", componentDef);
|
|
187
|
+
|
|
188
|
+
styleInjector.inject();
|
|
189
|
+
|
|
190
|
+
expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
|
|
191
|
+
expect(mockDocument.createElement).toHaveBeenCalledWith("style");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("should not inject CSS if no components have CSS", () => {
|
|
195
|
+
const componentDef: ComponentDefinition = {
|
|
196
|
+
type: "component",
|
|
197
|
+
component: {
|
|
198
|
+
interface: {},
|
|
199
|
+
structure: { type: "node", tag: "div" },
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
componentRegistry.register("TestComponent", componentDef);
|
|
204
|
+
|
|
205
|
+
styleInjector.inject();
|
|
206
|
+
|
|
207
|
+
expect(mockDocument.createElement).not.toHaveBeenCalled();
|
|
208
|
+
expect(mockDocument.head.appendChild).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test.skip("should handle template processing errors gracefully (requires @meno/studio ElementRegistry)", () => {
|
|
212
|
+
const componentDef: ComponentDefinition = {
|
|
213
|
+
type: "component",
|
|
214
|
+
component: {
|
|
215
|
+
interface: {},
|
|
216
|
+
structure: { type: "node", tag: "div" },
|
|
217
|
+
css: ".test { color: {{invalid.prop}}; }",
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
componentRegistry.register("TestComponent", componentDef);
|
|
222
|
+
|
|
223
|
+
const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
|
|
224
|
+
elementRegistry.register([0], mockElement as unknown as HTMLElement, "TestComponent", true, {
|
|
225
|
+
color: "red",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Should not throw, should fallback to original CSS
|
|
229
|
+
expect(() => styleInjector.inject()).not.toThrow();
|
|
230
|
+
expect(mockStyleTag.textContent).toContain("Component: TestComponent");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("should handle missing component props gracefully", () => {
|
|
234
|
+
const componentDef: ComponentDefinition = {
|
|
235
|
+
type: "component",
|
|
236
|
+
component: {
|
|
237
|
+
interface: {},
|
|
238
|
+
structure: { type: "node", tag: "div" },
|
|
239
|
+
css: ".test { color: {{color}}; }",
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
componentRegistry.register("TestComponent", componentDef);
|
|
244
|
+
|
|
245
|
+
// Register element without props
|
|
246
|
+
const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
|
|
247
|
+
elementRegistry.register([0], mockElement as unknown as HTMLElement, "TestComponent", true);
|
|
248
|
+
|
|
249
|
+
// Should not throw
|
|
250
|
+
expect(() => styleInjector.inject()).not.toThrow();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("should handle errors during injection gracefully", () => {
|
|
254
|
+
// Force an error by making getAll throw
|
|
255
|
+
const originalGetAll = componentRegistry.getAll.bind(componentRegistry);
|
|
256
|
+
componentRegistry.getAll = mock(() => {
|
|
257
|
+
throw new Error("Test error");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Should not throw - should handle error gracefully
|
|
261
|
+
expect(() => styleInjector.inject()).not.toThrow();
|
|
262
|
+
|
|
263
|
+
// Restore
|
|
264
|
+
componentRegistry.getAll = originalGetAll;
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("clear", () => {
|
|
269
|
+
test("should remove style tag if it exists", () => {
|
|
270
|
+
const existingStyle = createTypedMockHTMLElement({
|
|
271
|
+
tagName: "STYLE",
|
|
272
|
+
id: "component-css",
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
mockDocument.getElementById.mockReturnValueOnce(existingStyle);
|
|
276
|
+
|
|
277
|
+
styleInjector.clear();
|
|
278
|
+
|
|
279
|
+
expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
|
|
280
|
+
expect(existingStyle.remove).toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("should not throw if style tag does not exist", () => {
|
|
284
|
+
mockDocument.getElementById.mockReturnValueOnce(null);
|
|
285
|
+
|
|
286
|
+
expect(() => styleInjector.clear()).not.toThrow();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("exists", () => {
|
|
291
|
+
test("should return true if style tag exists", () => {
|
|
292
|
+
const existingStyle = createTypedMockHTMLElement({
|
|
293
|
+
tagName: "STYLE",
|
|
294
|
+
id: "component-css",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
mockDocument.getElementById.mockReturnValueOnce(existingStyle);
|
|
298
|
+
|
|
299
|
+
expect(styleInjector.exists()).toBe(true);
|
|
300
|
+
expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("should return false if style tag does not exist", () => {
|
|
304
|
+
mockDocument.getElementById.mockReturnValueOnce(null);
|
|
305
|
+
|
|
306
|
+
expect(styleInjector.exists()).toBe(false);
|
|
307
|
+
expect(mockDocument.getElementById).toHaveBeenCalledWith("component-css");
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("integration", () => {
|
|
312
|
+
test.skip("should handle full injection cycle (requires @meno/studio ElementRegistry)", () => {
|
|
313
|
+
const componentDef: ComponentDefinition = {
|
|
314
|
+
type: "component",
|
|
315
|
+
component: {
|
|
316
|
+
interface: {},
|
|
317
|
+
structure: { type: "node", tag: "div" },
|
|
318
|
+
css: ".test { color: {{color}}; }",
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
componentRegistry.register("TestComponent", componentDef);
|
|
323
|
+
|
|
324
|
+
const mockElement = createTypedMockHTMLElement({ tagName: "DIV" });
|
|
325
|
+
elementRegistry.register([0], mockElement as unknown as HTMLElement, "TestComponent", true, {
|
|
326
|
+
color: "green",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Inject - should create style tag
|
|
330
|
+
mockDocument.getElementById.mockReturnValueOnce(null); // No existing tag
|
|
331
|
+
styleInjector.inject();
|
|
332
|
+
expect(mockDocument.createElement).toHaveBeenCalledWith("style");
|
|
333
|
+
expect(mockDocument.head.appendChild).toHaveBeenCalled();
|
|
334
|
+
|
|
335
|
+
// Verify exists() works after injection
|
|
336
|
+
mockDocument.getElementById.mockReturnValueOnce(mockStyleTag);
|
|
337
|
+
expect(styleInjector.exists()).toBe(true);
|
|
338
|
+
|
|
339
|
+
// Clear - should remove style tag
|
|
340
|
+
const styleTagWithRemove = createTypedMockHTMLElement({
|
|
341
|
+
tagName: "STYLE",
|
|
342
|
+
id: "component-css",
|
|
343
|
+
});
|
|
344
|
+
mockDocument.getElementById.mockReturnValueOnce(styleTagWithRemove);
|
|
345
|
+
styleInjector.clear();
|
|
346
|
+
expect(styleTagWithRemove.remove).toHaveBeenCalled();
|
|
347
|
+
|
|
348
|
+
// Verify it's gone
|
|
349
|
+
mockDocument.getElementById.mockReturnValueOnce(null);
|
|
350
|
+
expect(styleInjector.exists()).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|