ezfw-core 1.0.30 โ 1.0.32
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/components/drawer/EzDrawer.module.scss +105 -0
- package/components/drawer/EzDrawer.ts +92 -0
- package/core/ez.ts +5 -0
- package/core/router.ts +13 -3
- package/core/services.ts +11 -0
- package/islands/StaticHtmlRenderer.test.ts +344 -0
- package/islands/StaticHtmlRenderer.ts +464 -0
- package/islands/ViteIslandsPlugin.ts +992 -0
- package/islands/analyzer.test.ts +295 -0
- package/islands/analyzer.ts +438 -0
- package/islands/examples/AddToCart.island.example.ts +129 -0
- package/islands/examples/ProductPage.example.ts +138 -0
- package/islands/examples/Reviews.island.example.ts +210 -0
- package/islands/index.ts +56 -0
- package/islands/runtime.ts +490 -0
- package/islands/ssrShim.js +86 -0
- package/islands/test-todo.ts +84 -0
- package/islands/vite.config.example.ts +68 -0
- package/modules.ts +2 -0
- package/package.json +66 -60
- package/services/drawer.js +103 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
.overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
background: rgba(0, 0, 0, 0.5);
|
|
8
|
+
z-index: 1000;
|
|
9
|
+
opacity: 0;
|
|
10
|
+
visibility: hidden;
|
|
11
|
+
transition: opacity 0.3s ease, visibility 0.3s ease;
|
|
12
|
+
|
|
13
|
+
&.open {
|
|
14
|
+
opacity: 1;
|
|
15
|
+
visibility: visible;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.drawer {
|
|
20
|
+
position: fixed;
|
|
21
|
+
z-index: 1001;
|
|
22
|
+
background: var(--ez-surface);
|
|
23
|
+
box-shadow: var(--ez-shadow-lg);
|
|
24
|
+
transition: transform 0.3s ease;
|
|
25
|
+
overflow: auto;
|
|
26
|
+
|
|
27
|
+
// Positions
|
|
28
|
+
&.left {
|
|
29
|
+
top: 0;
|
|
30
|
+
left: 0;
|
|
31
|
+
height: 100%;
|
|
32
|
+
transform: translateX(-100%);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&.right {
|
|
36
|
+
top: 0;
|
|
37
|
+
right: 0;
|
|
38
|
+
height: 100%;
|
|
39
|
+
transform: translateX(100%);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.top {
|
|
43
|
+
top: 0;
|
|
44
|
+
left: 0;
|
|
45
|
+
width: 100%;
|
|
46
|
+
transform: translateY(-100%);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&.bottom {
|
|
50
|
+
bottom: 0;
|
|
51
|
+
left: 0;
|
|
52
|
+
width: 100%;
|
|
53
|
+
transform: translateY(100%);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Open state
|
|
57
|
+
&.open {
|
|
58
|
+
&.left,
|
|
59
|
+
&.right {
|
|
60
|
+
transform: translateX(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&.top,
|
|
64
|
+
&.bottom {
|
|
65
|
+
transform: translateY(0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Sizes for left/right
|
|
70
|
+
&.left,
|
|
71
|
+
&.right {
|
|
72
|
+
&.sm {
|
|
73
|
+
width: 280px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&.md {
|
|
77
|
+
width: 400px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&.lg {
|
|
81
|
+
width: 600px;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sizes for top/bottom
|
|
86
|
+
&.top,
|
|
87
|
+
&.bottom {
|
|
88
|
+
&.sm {
|
|
89
|
+
height: 200px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&.md {
|
|
93
|
+
height: 300px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&.lg {
|
|
97
|
+
height: 450px;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.content {
|
|
103
|
+
height: 100%;
|
|
104
|
+
overflow: auto;
|
|
105
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import styles from './EzDrawer.module.scss';
|
|
2
|
+
import { cx } from '../../utils/cssModules.js';
|
|
3
|
+
import { EzBaseComponent } from '../EzBaseComponent.js';
|
|
4
|
+
|
|
5
|
+
const css = cx(styles);
|
|
6
|
+
|
|
7
|
+
declare const ez: {
|
|
8
|
+
_createElement(config: unknown): Promise<HTMLElement>;
|
|
9
|
+
define(name: string, component: unknown): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface DrawerInstance {
|
|
13
|
+
close: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface EzDrawerConfig {
|
|
17
|
+
props?: {
|
|
18
|
+
position?: 'left' | 'right' | 'top' | 'bottom';
|
|
19
|
+
size?: 'sm' | 'md' | 'lg' | string;
|
|
20
|
+
closeOnOverlayClick?: boolean;
|
|
21
|
+
onOverlayClick?: () => void;
|
|
22
|
+
items?: unknown[];
|
|
23
|
+
_drawerInstance?: DrawerInstance;
|
|
24
|
+
};
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class EzDrawer extends EzBaseComponent {
|
|
29
|
+
static eztype = 'EzDrawer';
|
|
30
|
+
declare config: EzDrawerConfig;
|
|
31
|
+
|
|
32
|
+
async render(): Promise<HTMLDivElement> {
|
|
33
|
+
const props = this.config.props || this.config;
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
position = 'left',
|
|
37
|
+
size = 'md',
|
|
38
|
+
closeOnOverlayClick = true,
|
|
39
|
+
onOverlayClick,
|
|
40
|
+
items = [],
|
|
41
|
+
_drawerInstance
|
|
42
|
+
} = props as EzDrawerConfig['props'] & { [key: string]: unknown };
|
|
43
|
+
|
|
44
|
+
const isPresetSize = ['sm', 'md', 'lg'].includes(size as string);
|
|
45
|
+
|
|
46
|
+
const overlay = await ez._createElement({
|
|
47
|
+
eztype: 'div',
|
|
48
|
+
cls: css('overlay'),
|
|
49
|
+
onClick: () => {
|
|
50
|
+
if (onOverlayClick) {
|
|
51
|
+
onOverlayClick();
|
|
52
|
+
}
|
|
53
|
+
if (closeOnOverlayClick) {
|
|
54
|
+
_drawerInstance?.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}) as HTMLDivElement;
|
|
58
|
+
|
|
59
|
+
const drawerClasses = [css('drawer'), css(position)];
|
|
60
|
+
if (isPresetSize) {
|
|
61
|
+
drawerClasses.push(css(size as string));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const drawerStyle: Record<string, string> = {};
|
|
65
|
+
if (!isPresetSize && size) {
|
|
66
|
+
if (position === 'left' || position === 'right') {
|
|
67
|
+
drawerStyle.width = size as string;
|
|
68
|
+
} else {
|
|
69
|
+
drawerStyle.height = size as string;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const drawer = await ez._createElement({
|
|
74
|
+
eztype: 'div',
|
|
75
|
+
cls: drawerClasses.join(' '),
|
|
76
|
+
style: Object.keys(drawerStyle).length ? drawerStyle : undefined,
|
|
77
|
+
onClick: (e: Event) => e.stopPropagation(),
|
|
78
|
+
items: [{
|
|
79
|
+
eztype: 'div',
|
|
80
|
+
cls: css('content'),
|
|
81
|
+
controller: this.config.controller,
|
|
82
|
+
items
|
|
83
|
+
}]
|
|
84
|
+
}) as HTMLDivElement;
|
|
85
|
+
|
|
86
|
+
overlay.appendChild(drawer);
|
|
87
|
+
|
|
88
|
+
return overlay;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ez.define('EzDrawer', EzDrawer);
|
package/core/ez.ts
CHANGED
|
@@ -218,6 +218,7 @@ export interface EzFramework {
|
|
|
218
218
|
dialog: unknown;
|
|
219
219
|
toast: unknown;
|
|
220
220
|
mask: unknown;
|
|
221
|
+
drawer: unknown;
|
|
221
222
|
setTheme(themeName: string): void;
|
|
222
223
|
getTheme(): string;
|
|
223
224
|
getAvailableThemes(): string[];
|
|
@@ -583,6 +584,10 @@ const ez: EzFramework = {
|
|
|
583
584
|
return this._services?.mask;
|
|
584
585
|
},
|
|
585
586
|
|
|
587
|
+
get drawer(): unknown {
|
|
588
|
+
return this._services?.drawer;
|
|
589
|
+
},
|
|
590
|
+
|
|
586
591
|
setTheme(themeName: string): void {
|
|
587
592
|
if (!this._availableThemes.includes(themeName)) {
|
|
588
593
|
console.warn(`[ez] Theme "${themeName}" not found. Available: ${this._availableThemes.join(', ')}`);
|
package/core/router.ts
CHANGED
|
@@ -206,11 +206,21 @@ export class EzRouter {
|
|
|
206
206
|
// If not found, try to load via the loader (dynamic import)
|
|
207
207
|
if (!def) {
|
|
208
208
|
const modules = this.ez._loader.getModules();
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
const viewLower = viewName.toLowerCase();
|
|
210
|
+
const moduleKeys = Object.keys(modules);
|
|
211
|
+
|
|
212
|
+
// Priority 1: Exact file match /ViewName.js
|
|
213
|
+
let path = moduleKeys.find(p =>
|
|
214
|
+
p.toLowerCase().endsWith(`/${viewLower}.js`)
|
|
212
215
|
);
|
|
213
216
|
|
|
217
|
+
// Priority 2: Folder structure /ViewName/ViewName.js
|
|
218
|
+
if (!path) {
|
|
219
|
+
path = moduleKeys.find(p =>
|
|
220
|
+
p.toLowerCase().endsWith(`/${viewLower}/${viewLower}.js`)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
214
224
|
if (path) {
|
|
215
225
|
await this.ez._loader.loadModule(path);
|
|
216
226
|
def = this.ez.get(viewName);
|
package/core/services.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RouteUIService } from '../services/RouteUI.js';
|
|
2
2
|
import { FetchApi } from '../services/fetchApi.js';
|
|
3
3
|
import { EzDialogService } from '../services/dialog.js';
|
|
4
|
+
import { EzDrawerService } from '../services/drawer.js';
|
|
4
5
|
import { EzToastService } from '../services/toast.js';
|
|
5
6
|
import { EzMaskService } from '../services/mask.js';
|
|
6
7
|
|
|
@@ -25,6 +26,7 @@ export class EzServices {
|
|
|
25
26
|
private _api: FetchApi | null = null;
|
|
26
27
|
private _routeUI: RouteUIService | null = null;
|
|
27
28
|
private _dialog: EzDialogService | null = null;
|
|
29
|
+
private _drawer: EzDrawerService | null = null;
|
|
28
30
|
private _toast: EzToastService | null = null;
|
|
29
31
|
private _mask: EzMaskService | null = null;
|
|
30
32
|
private _initialized: boolean = false;
|
|
@@ -41,6 +43,7 @@ export class EzServices {
|
|
|
41
43
|
this._initApi(),
|
|
42
44
|
this._initFirebase(),
|
|
43
45
|
this._initDialog(),
|
|
46
|
+
this._initDrawer(),
|
|
44
47
|
this._initToast(),
|
|
45
48
|
this._initMask()
|
|
46
49
|
]);
|
|
@@ -124,6 +127,14 @@ export class EzServices {
|
|
|
124
127
|
return this._dialog;
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
private _initDrawer(): void {
|
|
131
|
+
this._drawer = new EzDrawerService();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get drawer(): EzDrawerService | null {
|
|
135
|
+
return this._drawer;
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
private _initToast(): void {
|
|
128
139
|
this._toast = new EzToastService();
|
|
129
140
|
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ez Islands - Static HTML Renderer Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { StaticHtmlRenderer, ComponentDefinition, RenderContext } from './StaticHtmlRenderer.js';
|
|
6
|
+
import { ComponentAnalyzer } from './analyzer.js';
|
|
7
|
+
|
|
8
|
+
const renderer = new StaticHtmlRenderer();
|
|
9
|
+
|
|
10
|
+
// Reset island counter before each test suite
|
|
11
|
+
renderer.resetIslandCounter();
|
|
12
|
+
|
|
13
|
+
interface TestCase {
|
|
14
|
+
name: string;
|
|
15
|
+
component: ComponentDefinition;
|
|
16
|
+
props?: Record<string, unknown>;
|
|
17
|
+
registry?: Map<string, ComponentDefinition>;
|
|
18
|
+
expected: string | RegExp;
|
|
19
|
+
description: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const testCases: TestCase[] = [
|
|
23
|
+
// Basic static rendering
|
|
24
|
+
{
|
|
25
|
+
name: 'SimpleDiv',
|
|
26
|
+
component: {
|
|
27
|
+
template: () => ({
|
|
28
|
+
eztype: 'div',
|
|
29
|
+
cls: 'container',
|
|
30
|
+
text: 'Hello World'
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
expected: '<div class="container">Hello World</div>',
|
|
34
|
+
description: 'Simple div with class and text'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'NestedElements',
|
|
38
|
+
component: {
|
|
39
|
+
template: () => ({
|
|
40
|
+
eztype: 'article',
|
|
41
|
+
cls: 'post',
|
|
42
|
+
items: [
|
|
43
|
+
{ eztype: 'h1', text: 'Title' },
|
|
44
|
+
{ eztype: 'p', text: 'Content here' }
|
|
45
|
+
]
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
expected: '<article class="post"><h1>Title</h1><p>Content here</p></article>',
|
|
49
|
+
description: 'Nested elements with items'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'VoidElements',
|
|
53
|
+
component: {
|
|
54
|
+
template: () => ({
|
|
55
|
+
eztype: 'div',
|
|
56
|
+
items: [
|
|
57
|
+
{ eztype: 'img', src: '/image.jpg', alt: 'Test' },
|
|
58
|
+
{ eztype: 'br' },
|
|
59
|
+
{ eztype: 'input', type: 'text', placeholder: 'Enter text' }
|
|
60
|
+
]
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
expected: '<div><img src="/image.jpg" alt="Test"><br><input type="text" placeholder="Enter text"></div>',
|
|
64
|
+
description: 'Void elements render without closing tag'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'BooleanAttributes',
|
|
68
|
+
component: {
|
|
69
|
+
template: () => ({
|
|
70
|
+
eztype: 'input',
|
|
71
|
+
type: 'checkbox',
|
|
72
|
+
checked: true,
|
|
73
|
+
disabled: true
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
expected: '<input type="checkbox" checked disabled>',
|
|
77
|
+
description: 'Boolean attributes render correctly'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'StyleObject',
|
|
81
|
+
component: {
|
|
82
|
+
template: () => ({
|
|
83
|
+
eztype: 'div',
|
|
84
|
+
style: {
|
|
85
|
+
backgroundColor: 'red',
|
|
86
|
+
fontSize: '16px',
|
|
87
|
+
marginTop: '10px'
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
expected: '<div style="background-color: red; font-size: 16px; margin-top: 10px"></div>',
|
|
92
|
+
description: 'Style object converts to CSS string'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'EscapeHtml',
|
|
96
|
+
component: {
|
|
97
|
+
template: () => ({
|
|
98
|
+
eztype: 'div',
|
|
99
|
+
text: '<script>alert("xss")</script>'
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
expected: '<div><script>alert("xss")</script></div>',
|
|
103
|
+
description: 'HTML in text is escaped'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'EzComponentAsDiv',
|
|
107
|
+
component: {
|
|
108
|
+
template: () => ({
|
|
109
|
+
eztype: 'EzComponent',
|
|
110
|
+
cls: 'wrapper',
|
|
111
|
+
items: [
|
|
112
|
+
{ eztype: 'span', text: 'Inside' }
|
|
113
|
+
]
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
expected: '<div class="wrapper"><span>Inside</span></div>',
|
|
117
|
+
description: 'EzComponent renders as div'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'PropsInTemplate',
|
|
121
|
+
component: {
|
|
122
|
+
template: (props) => {
|
|
123
|
+
const p = props as { title: string; count: number };
|
|
124
|
+
return {
|
|
125
|
+
eztype: 'div',
|
|
126
|
+
items: [
|
|
127
|
+
{ eztype: 'h1', text: p.title },
|
|
128
|
+
{ eztype: 'span', text: `Count: ${p.count}` }
|
|
129
|
+
]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
props: { title: 'My Title', count: 42 },
|
|
134
|
+
expected: '<div><h1>My Title</h1><span>Count: 42</span></div>',
|
|
135
|
+
description: 'Props are passed to template'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'ArrayClasses',
|
|
139
|
+
component: {
|
|
140
|
+
template: () => ({
|
|
141
|
+
eztype: 'div',
|
|
142
|
+
cls: ['class1', 'class2', 'class3']
|
|
143
|
+
})
|
|
144
|
+
},
|
|
145
|
+
expected: '<div class="class1 class2 class3"></div>',
|
|
146
|
+
description: 'Array of classes joins correctly'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'NullItemsFiltered',
|
|
150
|
+
component: {
|
|
151
|
+
template: () => ({
|
|
152
|
+
eztype: 'div',
|
|
153
|
+
items: [
|
|
154
|
+
{ eztype: 'span', text: 'First' },
|
|
155
|
+
null,
|
|
156
|
+
{ eztype: 'span', text: 'Second' },
|
|
157
|
+
undefined
|
|
158
|
+
] as ComponentDefinition['items']
|
|
159
|
+
})
|
|
160
|
+
},
|
|
161
|
+
expected: '<div><span>First</span><span>Second</span></div>',
|
|
162
|
+
description: 'Null/undefined items are filtered out'
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// Island rendering
|
|
166
|
+
{
|
|
167
|
+
name: 'IslandWithState',
|
|
168
|
+
component: {
|
|
169
|
+
state: { count: 0 },
|
|
170
|
+
template: () => ({
|
|
171
|
+
eztype: 'button',
|
|
172
|
+
text: 'Click me'
|
|
173
|
+
})
|
|
174
|
+
},
|
|
175
|
+
expected: /data-ez-island="IslandWithState"/,
|
|
176
|
+
description: 'Component with state renders as island placeholder'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'IslandWithProps',
|
|
180
|
+
component: {
|
|
181
|
+
state: { loading: false },
|
|
182
|
+
template: (props) => ({
|
|
183
|
+
eztype: 'button',
|
|
184
|
+
text: `Add product ${(props as { productId: number }).productId}`
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
props: { productId: 123 },
|
|
188
|
+
expected: /data-ez-props="[^"]*productId[^"]*123/,
|
|
189
|
+
description: 'Island props are serialized in data attribute'
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'IslandInitialRender',
|
|
193
|
+
component: {
|
|
194
|
+
state: { count: 0 },
|
|
195
|
+
template: () => ({
|
|
196
|
+
eztype: 'div',
|
|
197
|
+
cls: 'counter',
|
|
198
|
+
text: 'Count: 0'
|
|
199
|
+
})
|
|
200
|
+
},
|
|
201
|
+
expected: /<div class="counter">Count: 0<\/div>/,
|
|
202
|
+
description: 'Island pre-renders initial content'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'ManualIslandOverride',
|
|
206
|
+
component: {
|
|
207
|
+
island: true,
|
|
208
|
+
template: () => ({
|
|
209
|
+
eztype: 'div',
|
|
210
|
+
text: 'Forced island'
|
|
211
|
+
})
|
|
212
|
+
},
|
|
213
|
+
expected: /data-ez-island="ManualIslandOverride"/,
|
|
214
|
+
description: 'Manual island: true forces island rendering'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'ManualStaticOverride',
|
|
218
|
+
component: {
|
|
219
|
+
static: true,
|
|
220
|
+
state: { value: '' }, // Would normally be island
|
|
221
|
+
template: () => ({
|
|
222
|
+
eztype: 'div',
|
|
223
|
+
text: 'Forced static'
|
|
224
|
+
})
|
|
225
|
+
},
|
|
226
|
+
expected: '<div>Forced static</div>',
|
|
227
|
+
description: 'Manual static: true forces static rendering'
|
|
228
|
+
}
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
// Run tests
|
|
232
|
+
console.log('\n๐งช Running Ez Islands Renderer Tests\n');
|
|
233
|
+
console.log('='.repeat(60));
|
|
234
|
+
|
|
235
|
+
let passed = 0;
|
|
236
|
+
let failed = 0;
|
|
237
|
+
|
|
238
|
+
async function runTests() {
|
|
239
|
+
for (const test of testCases) {
|
|
240
|
+
try {
|
|
241
|
+
const registry = test.registry || new Map([[test.name, test.component]]);
|
|
242
|
+
const ctx: RenderContext = {
|
|
243
|
+
registry,
|
|
244
|
+
styles: new Set(),
|
|
245
|
+
islands: [],
|
|
246
|
+
props: test.props || {}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const result = await renderer.renderComponent(test.name, ctx, test.props || {});
|
|
250
|
+
|
|
251
|
+
let success: boolean;
|
|
252
|
+
if (test.expected instanceof RegExp) {
|
|
253
|
+
success = test.expected.test(result);
|
|
254
|
+
} else {
|
|
255
|
+
success = result === test.expected;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (success) {
|
|
259
|
+
passed++;
|
|
260
|
+
console.log(`โ
${test.name}`);
|
|
261
|
+
console.log(` ${test.description}`);
|
|
262
|
+
} else {
|
|
263
|
+
failed++;
|
|
264
|
+
console.log(`โ ${test.name}`);
|
|
265
|
+
console.log(` ${test.description}`);
|
|
266
|
+
console.log(` Expected: ${test.expected}`);
|
|
267
|
+
console.log(` Got: ${result}`);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
failed++;
|
|
271
|
+
console.log(`โ ${test.name}`);
|
|
272
|
+
console.log(` ${test.description}`);
|
|
273
|
+
console.log(` Error: ${error}`);
|
|
274
|
+
}
|
|
275
|
+
console.log('');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log('='.repeat(60));
|
|
279
|
+
console.log(`\n๐ Results: ${passed} passed, ${failed} failed\n`);
|
|
280
|
+
|
|
281
|
+
// Test page rendering with hydration script
|
|
282
|
+
console.log('\n๐ Testing full page render with islands:\n');
|
|
283
|
+
|
|
284
|
+
renderer.resetIslandCounter();
|
|
285
|
+
|
|
286
|
+
const pageRegistry = new Map<string, ComponentDefinition>();
|
|
287
|
+
|
|
288
|
+
pageRegistry.set('Header', {
|
|
289
|
+
template: () => ({
|
|
290
|
+
eztype: 'header',
|
|
291
|
+
items: [{ eztype: 'h1', text: 'My App' }]
|
|
292
|
+
})
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
pageRegistry.set('AddToCart', {
|
|
296
|
+
state: { quantity: 1 },
|
|
297
|
+
template: (props) => ({
|
|
298
|
+
eztype: 'button',
|
|
299
|
+
text: `Add to cart (${(props as { productId: number }).productId})`
|
|
300
|
+
})
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
pageRegistry.set('ProductPage', {
|
|
304
|
+
template: (props) => {
|
|
305
|
+
const p = props as { product: { id: number; name: string } };
|
|
306
|
+
return {
|
|
307
|
+
eztype: 'main',
|
|
308
|
+
items: [
|
|
309
|
+
{ eztype: 'Header' },
|
|
310
|
+
{ eztype: 'h2', text: p.product.name },
|
|
311
|
+
{ eztype: 'AddToCart', props: { productId: p.product.id } }
|
|
312
|
+
]
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const ctx: RenderContext = {
|
|
318
|
+
registry: pageRegistry,
|
|
319
|
+
styles: new Set(),
|
|
320
|
+
islands: [],
|
|
321
|
+
props: {}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const pageHtml = await renderer.renderComponent(
|
|
325
|
+
'ProductPage',
|
|
326
|
+
ctx,
|
|
327
|
+
{ product: { id: 42, name: 'iPhone 15' } }
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
console.log('Generated HTML:');
|
|
331
|
+
console.log('-'.repeat(40));
|
|
332
|
+
console.log(pageHtml);
|
|
333
|
+
console.log('-'.repeat(40));
|
|
334
|
+
console.log(`\nIslands found: ${ctx.islands.length}`);
|
|
335
|
+
ctx.islands.forEach(island => {
|
|
336
|
+
console.log(` ๐๏ธ ${island.component} (id: ${island.id})`);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (failed > 0) {
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
runTests().catch(console.error);
|