ripple 0.2.210 → 0.2.212

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.
@@ -0,0 +1,37 @@
1
+ export component Layout({ children }) {
2
+ <div class="layout">
3
+ <children />
4
+ </div>
5
+ }
6
+
7
+ export component SingleChild() {
8
+ <div class="single">{'single'}</div>
9
+ }
10
+
11
+ export component MultiRootChild() {
12
+ <h1>{'title'}</h1>
13
+ <p>{'description'}</p>
14
+ }
15
+
16
+ export component EmptyLayout() {
17
+ <Layout />
18
+ }
19
+
20
+ export component LayoutWithSingleChild() {
21
+ <Layout>
22
+ <SingleChild />
23
+ </Layout>
24
+ }
25
+
26
+ export component LayoutWithMultipleChildren() {
27
+ <Layout>
28
+ <SingleChild />
29
+ <div class="extra">{'extra'}</div>
30
+ </Layout>
31
+ }
32
+
33
+ export component LayoutWithMultiRootChild() {
34
+ <Layout>
35
+ <MultiRootChild />
36
+ </Layout>
37
+ }
@@ -0,0 +1,196 @@
1
+ // Minimal repro for hydration issue with if block containing children
2
+ // Based on SidebarGroup pattern from website-new
3
+ import { track } from 'ripple';
4
+
5
+ export component IfWithChildren({ children }: { children: any }) {
6
+ let expanded = track(true);
7
+
8
+ <div class="container">
9
+ <div class="header" role="button" onClick={() => (@expanded = !@expanded)}>{'Toggle'}</div>
10
+ if (@expanded) {
11
+ <div class="content">
12
+ <children />
13
+ </div>
14
+ }
15
+ </div>
16
+ }
17
+
18
+ export component ChildItem({ text }: { text: string }) {
19
+ <div class="item">{text}</div>
20
+ }
21
+
22
+ export component TestIfWithChildren() {
23
+ <IfWithChildren>
24
+ <ChildItem text="Item 1" />
25
+ <ChildItem text="Item 2" />
26
+ </IfWithChildren>
27
+ }
28
+
29
+ // Simpler variant - if block with static children
30
+ export component IfWithStaticChildren() {
31
+ let expanded = track(true);
32
+
33
+ <div class="container">
34
+ <div class="header" role="button" onClick={() => (@expanded = !@expanded)}>{'Toggle'}</div>
35
+ if (@expanded) {
36
+ <div class="content">
37
+ <span>{'Static child 1'}</span>
38
+ <span>{'Static child 2'}</span>
39
+ </div>
40
+ }
41
+ </div>
42
+ }
43
+
44
+ // Variant with sibling elements before the if block (like SidebarGroup)
45
+ export component IfWithSiblingsAndChildren({ children }: { children: any }) {
46
+ let expanded = track(true);
47
+
48
+ <section class="group">
49
+ <div class="item" role="button" onClick={() => (@expanded = !@expanded)}>
50
+ <div class="indicator" />
51
+ <h2 class="text">{'Title'}</h2>
52
+ <div class="caret">
53
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
54
+ <path d="m9 18 6-6-6-6" />
55
+ </svg>
56
+ </div>
57
+ </div>
58
+ if (@expanded) {
59
+ <div class="items">
60
+ <children />
61
+ </div>
62
+ }
63
+ </section>
64
+ }
65
+
66
+ export component TestIfWithSiblingsAndChildren() {
67
+ <IfWithSiblingsAndChildren>
68
+ <ChildItem text="Item A" />
69
+ <ChildItem text="Item B" />
70
+ </IfWithSiblingsAndChildren>
71
+ }
72
+
73
+ // Test case for hydration pop bug: element with nested children followed by dynamic if sibling
74
+ // This tests that hydrate_node is properly restored after processing an element's children
75
+ // before navigating to a dynamic sibling (if/for/switch)
76
+ export component ElementWithChildrenThenIf() {
77
+ let show = track(true);
78
+
79
+ <div class="wrapper">
80
+ <div class="nested-parent">
81
+ <div class="nested-child">
82
+ <span class="deep">{'Deep content'}</span>
83
+ </div>
84
+ </div>
85
+ if (@show) {
86
+ <div class="conditional">{'Conditional content'}</div>
87
+ }
88
+ </div>
89
+
90
+ <button class="toggle" onClick={() => (@show = !@show)}>{'Toggle'}</button>
91
+ }
92
+
93
+ // More complex: multiple levels of nesting before if sibling
94
+ export component DeepNestingThenIf() {
95
+ let visible = track(true);
96
+
97
+ <section class="outer">
98
+ <article class="middle">
99
+ <div class="inner">
100
+ <p class="leaf">
101
+ <strong>{'Bold'}</strong>
102
+ <em>{'Italic'}</em>
103
+ </p>
104
+ </div>
105
+ </article>
106
+ if (@visible) {
107
+ <footer class="footer">{'Footer'}</footer>
108
+ }
109
+ </section>
110
+
111
+ <button class="btn" onClick={() => (@visible = !@visible)}>{'Toggle'}</button>
112
+ }
113
+
114
+ // Test case for CodeBlock pattern: element with only DOM element children (like buttons)
115
+ // followed by another sibling element. This requires pop() to restore hydrate_node
116
+ // because we descend into the first element to get the button children.
117
+ export component DomElementChildrenThenSibling() {
118
+ let activeTab = track('code');
119
+
120
+ <div class="tabs">
121
+ <div class="tab-list">
122
+ <button
123
+ class="tab"
124
+ aria-selected={@activeTab === 'code' ? 'true' : 'false'}
125
+ onClick={() => (@activeTab = 'code')}
126
+ >
127
+ {'Code'}
128
+ </button>
129
+ <button
130
+ class="tab"
131
+ aria-selected={@activeTab === 'preview' ? 'true' : 'false'}
132
+ onClick={() => (@activeTab = 'preview')}
133
+ >
134
+ {'Preview'}
135
+ </button>
136
+ </div>
137
+ <div class="panel">
138
+ if (@activeTab === 'code') {
139
+ <pre class="code">{'const x = 1;'}</pre>
140
+ } else {
141
+ <div class="preview">{'Preview content'}</div>
142
+ }
143
+ </div>
144
+ </div>
145
+ }
146
+
147
+ // Test case for element with DOM children followed by static siblings that don't
148
+ // generate sibling() calls. This was causing incorrect pop() generation before next().
149
+ // Pattern: <ul> with dynamic <li> children -> static <h2> -> static <p> -> next()
150
+ export component DomChildrenThenStaticSiblings() {
151
+ let count = track(0);
152
+
153
+ <div class="container">
154
+ <ul class="list">
155
+ <li class="item">
156
+ {'Item count: '}
157
+ {@count}
158
+ </li>
159
+ <li class="item">{'Another item'}</li>
160
+ </ul>
161
+ <h2 class="heading">{'Static Heading'}</h2>
162
+ <p class="para">{'Static paragraph'}</p>
163
+ </div>
164
+
165
+ <button class="inc" onClick={() => @count++}>{'Increment'}</button>
166
+ }
167
+
168
+ // Test case for completely static element children followed by static siblings.
169
+ // Pattern from introduction page: <ul> with static <li> (strong, code, text)
170
+ // followed by static <h2> and <p>. No pop() should be generated for these.
171
+ export component StaticListThenStaticSiblings() {
172
+ <div class="wrapper">
173
+ <ul class="features">
174
+ <li>
175
+ <strong>{'Feature One'}</strong>
176
+ {': Description of feature one with '}
177
+ <code>{'code'}</code>
178
+ {' reference'}
179
+ </li>
180
+ <li>
181
+ <strong>{'Feature Two'}</strong>
182
+ {': Another feature description'}
183
+ </li>
184
+ <li>
185
+ <strong>{'Feature Three'}</strong>
186
+ {': Third feature'}
187
+ </li>
188
+ </ul>
189
+ <h2 class="section-heading">{'Section Heading'}</h2>
190
+ <p class="section-content">
191
+ {'Static paragraph with '}
192
+ <a href="/link">{'a link'}</a>
193
+ {' and more text.'}
194
+ </p>
195
+ </div>
196
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { hydrateComponent, container } from '../setup-hydration.js';
3
+
4
+ import * as ServerComponents from './compiled/server/composite.js';
5
+ import * as ClientComponents from './compiled/client/composite.js';
6
+
7
+ describe('hydration > composite', () => {
8
+ it('hydrates a layout with no children', async () => {
9
+ await hydrateComponent(ServerComponents.EmptyLayout, ClientComponents.EmptyLayout);
10
+ expect(container.innerHTML).toBeHtml('<div class=\"layout\"></div>');
11
+ });
12
+
13
+ it('hydrates a layout with a single child component', async () => {
14
+ await hydrateComponent(
15
+ ServerComponents.LayoutWithSingleChild,
16
+ ClientComponents.LayoutWithSingleChild,
17
+ );
18
+ expect(container.innerHTML).toBeHtml(
19
+ '<div class=\"layout\"><div class=\"single\">single</div></div>',
20
+ );
21
+ });
22
+
23
+ it('hydrates a layout with multiple children', async () => {
24
+ await hydrateComponent(
25
+ ServerComponents.LayoutWithMultipleChildren,
26
+ ClientComponents.LayoutWithMultipleChildren,
27
+ );
28
+ expect(container.innerHTML).toBeHtml(
29
+ '<div class=\"layout\"><div class=\"single\">single</div><div class=\"extra\">extra</div></div>',
30
+ );
31
+ });
32
+
33
+ it('hydrates a layout with a child that has multiple roots', async () => {
34
+ await hydrateComponent(
35
+ ServerComponents.LayoutWithMultiRootChild,
36
+ ClientComponents.LayoutWithMultiRootChild,
37
+ );
38
+ expect(container.innerHTML).toBeHtml(
39
+ '<div class=\"layout\"><h1>title</h1><p>description</p></div>',
40
+ );
41
+ });
42
+ });
@@ -0,0 +1,272 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { flushSync } from 'ripple';
3
+ import { hydrateComponent, container } from '../setup-hydration.js';
4
+
5
+ // Import server-compiled components
6
+ import * as ServerComponents from './compiled/server/if-children.js';
7
+ // Import client-compiled components
8
+ import * as ClientComponents from './compiled/client/if-children.js';
9
+
10
+ describe('hydration > if blocks with children', () => {
11
+ it('hydrates if block containing component children', async () => {
12
+ await hydrateComponent(
13
+ ServerComponents.TestIfWithChildren,
14
+ ClientComponents.TestIfWithChildren,
15
+ );
16
+
17
+ // Should render the children initially (expanded = true)
18
+ const items = container.querySelectorAll('.item');
19
+ expect(items.length).toBe(2);
20
+ expect(items[0]?.textContent).toBe('Item 1');
21
+ expect(items[1]?.textContent).toBe('Item 2');
22
+ });
23
+
24
+ it('hydrates if block with static children', async () => {
25
+ await hydrateComponent(
26
+ ServerComponents.IfWithStaticChildren,
27
+ ClientComponents.IfWithStaticChildren,
28
+ );
29
+
30
+ const content = container.querySelector('.content');
31
+ expect(content).not.toBeNull();
32
+ expect(content?.querySelectorAll('span').length).toBe(2);
33
+ });
34
+
35
+ it('toggles if block with component children after hydration', async () => {
36
+ await hydrateComponent(
37
+ ServerComponents.TestIfWithChildren,
38
+ ClientComponents.TestIfWithChildren,
39
+ );
40
+
41
+ // Initially expanded
42
+ expect(container.querySelectorAll('.item').length).toBe(2);
43
+
44
+ // Click to collapse
45
+ container.querySelector('.header')?.click();
46
+ flushSync();
47
+
48
+ // Children should be hidden
49
+ expect(container.querySelectorAll('.item').length).toBe(0);
50
+
51
+ // Click to expand
52
+ container.querySelector('.header')?.click();
53
+ flushSync();
54
+
55
+ // Children should be visible again
56
+ expect(container.querySelectorAll('.item').length).toBe(2);
57
+ });
58
+
59
+ it('hydrates if block with siblings and children (SidebarGroup pattern)', async () => {
60
+ await hydrateComponent(
61
+ ServerComponents.TestIfWithSiblingsAndChildren,
62
+ ClientComponents.TestIfWithSiblingsAndChildren,
63
+ );
64
+
65
+ // Should have the section structure
66
+ expect(container.querySelector('section.group')).not.toBeNull();
67
+ expect(container.querySelector('.item')).not.toBeNull();
68
+ expect(container.querySelector('.caret')).not.toBeNull();
69
+
70
+ // Children should be rendered inside .items
71
+ const items = container.querySelectorAll('.items .item');
72
+ expect(items.length).toBe(2);
73
+ expect(items[0]?.textContent).toBe('Item A');
74
+ expect(items[1]?.textContent).toBe('Item B');
75
+ });
76
+
77
+ it('toggles if block with siblings and children (SidebarGroup pattern)', async () => {
78
+ await hydrateComponent(
79
+ ServerComponents.TestIfWithSiblingsAndChildren,
80
+ ClientComponents.TestIfWithSiblingsAndChildren,
81
+ );
82
+
83
+ // Initially expanded
84
+ expect(container.querySelectorAll('.items .item').length).toBe(2);
85
+
86
+ // Click the .item div (not .item inside .items!) to toggle
87
+ container.querySelector('section.group > .item')?.click();
88
+ flushSync();
89
+
90
+ // Children should be hidden
91
+ expect(container.querySelector('.items')).toBeNull();
92
+
93
+ // Click to expand
94
+ container.querySelector('section.group > .item')?.click();
95
+ flushSync();
96
+
97
+ // Children should be visible again
98
+ expect(container.querySelectorAll('.items .item').length).toBe(2);
99
+ });
100
+
101
+ // Tests for hydration pop bug: element with nested children followed by dynamic if sibling
102
+ // This ensures hydrate_node is properly restored after processing an element's children
103
+ // before navigating to a dynamic sibling
104
+
105
+ it('hydrates element with nested children followed by if sibling', async () => {
106
+ await hydrateComponent(
107
+ ServerComponents.ElementWithChildrenThenIf,
108
+ ClientComponents.ElementWithChildrenThenIf,
109
+ );
110
+
111
+ // Verify structure hydrated correctly
112
+ expect(container.querySelector('.nested-parent')).not.toBeNull();
113
+ expect(container.querySelector('.nested-child')).not.toBeNull();
114
+ expect(container.querySelector('.deep')?.textContent).toBe('Deep content');
115
+ expect(container.querySelector('.conditional')?.textContent).toBe('Conditional content');
116
+ });
117
+
118
+ it('toggles if sibling after element with nested children', async () => {
119
+ await hydrateComponent(
120
+ ServerComponents.ElementWithChildrenThenIf,
121
+ ClientComponents.ElementWithChildrenThenIf,
122
+ );
123
+
124
+ // Initially visible
125
+ expect(container.querySelector('.conditional')).not.toBeNull();
126
+
127
+ // Toggle off
128
+ container.querySelector('.toggle')?.click();
129
+ flushSync();
130
+
131
+ // If content should be hidden, nested content should remain
132
+ expect(container.querySelector('.conditional')).toBeNull();
133
+ expect(container.querySelector('.deep')?.textContent).toBe('Deep content');
134
+
135
+ // Toggle back on
136
+ container.querySelector('.toggle')?.click();
137
+ flushSync();
138
+
139
+ expect(container.querySelector('.conditional')?.textContent).toBe('Conditional content');
140
+ });
141
+
142
+ it('hydrates deeply nested element followed by if sibling', async () => {
143
+ await hydrateComponent(ServerComponents.DeepNestingThenIf, ClientComponents.DeepNestingThenIf);
144
+
145
+ // Verify deep nesting structure
146
+ expect(container.querySelector('.outer')).not.toBeNull();
147
+ expect(container.querySelector('.middle')).not.toBeNull();
148
+ expect(container.querySelector('.inner')).not.toBeNull();
149
+ expect(container.querySelector('.leaf strong')?.textContent).toBe('Bold');
150
+ expect(container.querySelector('.leaf em')?.textContent).toBe('Italic');
151
+ expect(container.querySelector('.footer')?.textContent).toBe('Footer');
152
+ });
153
+
154
+ it('toggles if sibling after deeply nested element', async () => {
155
+ await hydrateComponent(ServerComponents.DeepNestingThenIf, ClientComponents.DeepNestingThenIf);
156
+
157
+ // Initially visible
158
+ expect(container.querySelector('.footer')).not.toBeNull();
159
+
160
+ // Toggle off
161
+ container.querySelector('.btn')?.click();
162
+ flushSync();
163
+
164
+ // Footer should be hidden, nested content should remain
165
+ expect(container.querySelector('.footer')).toBeNull();
166
+ expect(container.querySelector('.leaf strong')?.textContent).toBe('Bold');
167
+
168
+ // Toggle back on
169
+ container.querySelector('.btn')?.click();
170
+ flushSync();
171
+
172
+ expect(container.querySelector('.footer')?.textContent).toBe('Footer');
173
+ });
174
+
175
+ // Test for CodeBlock pattern: element with only DOM element children (buttons)
176
+ // followed by another sibling element
177
+
178
+ it('hydrates element with DOM element children followed by sibling (CodeBlock pattern)', async () => {
179
+ await hydrateComponent(
180
+ ServerComponents.DomElementChildrenThenSibling,
181
+ ClientComponents.DomElementChildrenThenSibling,
182
+ );
183
+
184
+ // Verify structure hydrated correctly
185
+ expect(container.querySelector('.tabs')).not.toBeNull();
186
+ expect(container.querySelector('.tab-list')).not.toBeNull();
187
+ expect(container.querySelectorAll('.tab').length).toBe(2);
188
+ expect(container.querySelector('.panel')).not.toBeNull();
189
+ expect(container.querySelector('.code')?.textContent).toBe('const x = 1;');
190
+ });
191
+
192
+ it('switches tabs in CodeBlock pattern after hydration', async () => {
193
+ await hydrateComponent(
194
+ ServerComponents.DomElementChildrenThenSibling,
195
+ ClientComponents.DomElementChildrenThenSibling,
196
+ );
197
+
198
+ // Initially on 'code' tab
199
+ expect(container.querySelector('.code')).not.toBeNull();
200
+ expect(container.querySelector('.preview')).toBeNull();
201
+
202
+ // Click preview tab
203
+ const tabs = container.querySelectorAll('.tab');
204
+ tabs[1]?.click();
205
+ flushSync();
206
+
207
+ // Should show preview, hide code
208
+ expect(container.querySelector('.code')).toBeNull();
209
+ expect(container.querySelector('.preview')?.textContent).toBe('Preview content');
210
+
211
+ // Click code tab
212
+ tabs[0]?.click();
213
+ flushSync();
214
+
215
+ // Should show code again
216
+ expect(container.querySelector('.code')?.textContent).toBe('const x = 1;');
217
+ });
218
+
219
+ // Test for element with DOM children followed by static siblings that don't
220
+ // generate sibling() calls. This was causing incorrect pop() generation before next().
221
+ it('hydrates element with DOM children followed by static siblings', async () => {
222
+ await hydrateComponent(
223
+ ServerComponents.DomChildrenThenStaticSiblings,
224
+ ClientComponents.DomChildrenThenStaticSiblings,
225
+ );
226
+
227
+ // Verify structure hydrated correctly
228
+ expect(container.querySelector('.container')).not.toBeNull();
229
+ expect(container.querySelector('.list')).not.toBeNull();
230
+ expect(container.querySelectorAll('.item').length).toBe(2);
231
+ expect(container.querySelector('.heading')?.textContent).toBe('Static Heading');
232
+ expect(container.querySelector('.para')?.textContent).toBe('Static paragraph');
233
+ });
234
+
235
+ it('updates reactive content in element with DOM children followed by static siblings', async () => {
236
+ await hydrateComponent(
237
+ ServerComponents.DomChildrenThenStaticSiblings,
238
+ ClientComponents.DomChildrenThenStaticSiblings,
239
+ );
240
+
241
+ // Initially count is 0
242
+ const items = container.querySelectorAll('.item');
243
+ expect(items[0]?.textContent).toBe('Item count: 0');
244
+
245
+ // Increment count
246
+ container.querySelector('.inc')?.click();
247
+ flushSync();
248
+
249
+ // Count should update, static siblings should remain unchanged
250
+ expect(items[0]?.textContent).toBe('Item count: 1');
251
+ expect(container.querySelector('.heading')?.textContent).toBe('Static Heading');
252
+ expect(container.querySelector('.para')?.textContent).toBe('Static paragraph');
253
+ });
254
+
255
+ // Test for completely static content - introduction page pattern
256
+ // No pop() should be generated for static elements
257
+ it('hydrates static list followed by static siblings (intro page pattern)', async () => {
258
+ await hydrateComponent(
259
+ ServerComponents.StaticListThenStaticSiblings,
260
+ ClientComponents.StaticListThenStaticSiblings,
261
+ );
262
+
263
+ // Verify static structure hydrated correctly
264
+ expect(container.querySelector('.wrapper')).not.toBeNull();
265
+ expect(container.querySelector('.features')).not.toBeNull();
266
+ expect(container.querySelectorAll('li').length).toBe(3);
267
+ expect(container.querySelector('li strong')?.textContent).toBe('Feature One');
268
+ expect(container.querySelector('li code')?.textContent).toBe('code');
269
+ expect(container.querySelector('.section-heading')?.textContent).toBe('Section Heading');
270
+ expect(container.querySelector('.section-content a')?.textContent).toBe('a link');
271
+ });
272
+ });