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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -0
- package/package.json +5 -2
- package/src/compiler/phases/1-parse/index.js +9 -1
- package/src/compiler/phases/3-transform/client/index.js +145 -4
- package/src/compiler/types/index.d.ts +0 -6
- package/src/compiler/types/rpc.d.ts +5 -0
- package/src/runtime/internal/client/hydration.js +4 -0
- package/src/runtime/internal/client/rpc.js +31 -3
- package/src/runtime/internal/client/template.js +6 -3
- package/tests/client/compiler/compiler.try-in-function.test.ripple +159 -0
- package/tests/hydration/basic.test.js +23 -0
- package/tests/hydration/compiled/client/basic.js +65 -0
- package/tests/hydration/compiled/client/composite.js +139 -0
- package/tests/hydration/compiled/client/if-children.js +406 -0
- package/tests/hydration/compiled/client/portal.js +3 -0
- package/tests/hydration/compiled/server/basic.js +106 -0
- package/tests/hydration/compiled/server/composite.js +176 -0
- package/tests/hydration/compiled/server/if-children.js +685 -0
- package/tests/hydration/components/basic.ripple +30 -0
- package/tests/hydration/components/composite.ripple +37 -0
- package/tests/hydration/components/if-children.ripple +196 -0
- package/tests/hydration/composite.test.js +42 -0
- package/tests/hydration/if-children.test.js +272 -0
|
@@ -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
|
+
});
|