ripple 0.3.8 → 0.3.10

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +38 -172
  4. package/src/compiler/phases/2-analyze/index.js +308 -115
  5. package/src/compiler/phases/2-analyze/prune.js +13 -5
  6. package/src/compiler/phases/3-transform/client/index.js +197 -213
  7. package/src/compiler/phases/3-transform/segments.js +0 -7
  8. package/src/compiler/phases/3-transform/server/index.js +77 -170
  9. package/src/compiler/types/acorn.d.ts +1 -1
  10. package/src/compiler/types/estree.d.ts +1 -1
  11. package/src/compiler/types/import.d.ts +0 -2
  12. package/src/compiler/types/index.d.ts +14 -18
  13. package/src/compiler/types/parse.d.ts +3 -9
  14. package/src/compiler/utils.js +154 -21
  15. package/src/runtime/element.js +39 -0
  16. package/src/runtime/index-client.js +2 -13
  17. package/src/runtime/index-server.js +2 -2
  18. package/src/runtime/internal/client/bindings.js +3 -1
  19. package/src/runtime/internal/client/composite.js +11 -6
  20. package/src/runtime/internal/client/events.js +1 -1
  21. package/src/runtime/internal/client/expression.js +218 -0
  22. package/src/runtime/internal/client/head.js +3 -4
  23. package/src/runtime/internal/client/index.js +4 -1
  24. package/src/runtime/internal/client/portal.js +12 -6
  25. package/src/runtime/internal/client/runtime.js +0 -52
  26. package/src/runtime/internal/server/index.js +57 -56
  27. package/tests/client/basic/basic.components.test.ripple +85 -87
  28. package/tests/client/basic/basic.errors.test.ripple +28 -4
  29. package/tests/client/basic/basic.reactivity.test.ripple +10 -155
  30. package/tests/client/basic/basic.rendering.test.ripple +23 -8
  31. package/tests/client/capture-error.js +12 -0
  32. package/tests/client/compiler/compiler.basic.test.ripple +107 -18
  33. package/tests/client/composite/composite.props.test.ripple +5 -9
  34. package/tests/client/composite/composite.reactivity.test.ripple +35 -36
  35. package/tests/client/composite/composite.render.test.ripple +45 -13
  36. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  37. package/tests/client/dynamic-elements.test.ripple +3 -4
  38. package/tests/client/lazy-destructuring.test.ripple +69 -12
  39. package/tests/client/svg.test.ripple +4 -4
  40. package/tests/hydration/basic.test.js +23 -0
  41. package/tests/hydration/compiled/client/basic.js +118 -66
  42. package/tests/hydration/compiled/client/composite.js +90 -37
  43. package/tests/hydration/compiled/client/events.js +18 -18
  44. package/tests/hydration/compiled/client/for.js +62 -62
  45. package/tests/hydration/compiled/client/head.js +10 -10
  46. package/tests/hydration/compiled/client/hmr.js +13 -10
  47. package/tests/hydration/compiled/client/html.js +274 -236
  48. package/tests/hydration/compiled/client/if-children.js +41 -35
  49. package/tests/hydration/compiled/client/if.js +2 -2
  50. package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
  51. package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
  52. package/tests/hydration/compiled/client/portal.js +8 -8
  53. package/tests/hydration/compiled/client/reactivity.js +14 -14
  54. package/tests/hydration/compiled/client/return.js +2 -2
  55. package/tests/hydration/compiled/client/try.js +4 -4
  56. package/tests/hydration/compiled/server/basic.js +64 -31
  57. package/tests/hydration/compiled/server/composite.js +62 -29
  58. package/tests/hydration/compiled/server/hmr.js +24 -37
  59. package/tests/hydration/compiled/server/html.js +472 -611
  60. package/tests/hydration/compiled/server/if-children.js +77 -103
  61. package/tests/hydration/compiled/server/portal.js +8 -8
  62. package/tests/hydration/components/basic.ripple +15 -5
  63. package/tests/hydration/components/composite.ripple +13 -1
  64. package/tests/hydration/components/hmr.ripple +1 -3
  65. package/tests/hydration/components/html.ripple +13 -35
  66. package/tests/hydration/components/if-children.ripple +4 -8
  67. package/tests/hydration/composite.test.js +11 -0
  68. package/tests/server/basic.attributes.test.ripple +50 -0
  69. package/tests/server/basic.components.test.ripple +22 -28
  70. package/tests/server/basic.test.ripple +12 -0
  71. package/tests/server/compiler.test.ripple +43 -4
  72. package/tests/server/composite.props.test.ripple +5 -9
  73. package/tests/server/dynamic-elements.test.ripple +3 -4
  74. package/tests/server/lazy-destructuring.test.ripple +68 -12
  75. package/tests/server/style-identifier.test.ripple +2 -4
  76. package/tsconfig.typecheck.json +4 -0
  77. package/types/index.d.ts +9 -21
  78. package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
  79. package/tests/client/tracked-expression.test.ripple +0 -26
@@ -3,68 +3,55 @@ import * as _$_ from 'ripple/internal/server';
3
3
 
4
4
  import { track } from 'ripple/server';
5
5
 
6
- export async function IfWithChildren(__output, { children }) {
7
- return _$_.async(async () => {
8
- _$_.push_component();
6
+ export function IfWithChildren(__output, { children }) {
7
+ _$_.push_component();
8
+
9
+ let lazy = _$_.track(true);
9
10
 
10
- let lazy = _$_.track(true);
11
+ __output.push('<div');
12
+ __output.push(' class="container"');
13
+ __output.push('>');
11
14
 
15
+ {
12
16
  __output.push('<div');
13
- __output.push(' class="container"');
17
+ __output.push(' role="button"');
18
+ __output.push(' class="header"');
14
19
  __output.push('>');
15
20
 
16
21
  {
22
+ __output.push('Toggle');
23
+ }
24
+
25
+ __output.push('</div>');
26
+ __output.push('<!--[-->');
27
+
28
+ if (_$_.get(lazy)) {
17
29
  __output.push('<div');
18
- __output.push(' role="button"');
19
- __output.push(' class="header"');
30
+ __output.push(' class="content"');
20
31
  __output.push('>');
21
32
 
22
33
  {
23
- __output.push('Toggle');
34
+ _$_.render_expression(__output, children);
24
35
  }
25
36
 
26
37
  __output.push('</div>');
27
- __output.push('<!--[-->');
28
-
29
- if (_$_.get(lazy)) {
30
- __output.push('<div');
31
- __output.push(' class="content"');
32
- __output.push('>');
33
-
34
- {
35
- {
36
- const comp = children;
37
- const args = [__output, {}];
38
-
39
- if (comp?.async) {
40
- await comp(...args);
41
- } else if (comp) {
42
- comp(...args);
43
- }
44
- }
45
- }
46
-
47
- __output.push('</div>');
48
- }
49
-
50
- __output.push('<!--]-->');
51
38
  }
52
39
 
53
- __output.push('</div>');
54
- _$_.pop_component();
55
- });
56
- }
40
+ __output.push('<!--]-->');
41
+ }
57
42
 
58
- IfWithChildren.async = true;
43
+ __output.push('</div>');
44
+ _$_.pop_component();
45
+ }
59
46
 
60
- export function ChildItem(__output, { text }) {
47
+ export function ChildItem(__output, { text: label }) {
61
48
  _$_.push_component();
62
49
  __output.push('<div');
63
50
  __output.push(' class="item"');
64
51
  __output.push('>');
65
52
 
66
53
  {
67
- __output.push(_$_.escape(text));
54
+ __output.push(_$_.escape(label));
68
55
  }
69
56
 
70
57
  __output.push('</div>');
@@ -80,7 +67,7 @@ export function TestIfWithChildren(__output) {
80
67
  const args = [
81
68
  __output,
82
69
  {
83
- children: function children(__output) {
70
+ children: _$_.ripple_element(function render_children(__output) {
84
71
  _$_.push_component();
85
72
 
86
73
  {
@@ -98,7 +85,7 @@ export function TestIfWithChildren(__output) {
98
85
  }
99
86
 
100
87
  _$_.pop_component();
101
- }
88
+ })
102
89
  }
103
90
  ];
104
91
 
@@ -164,94 +151,81 @@ export function IfWithStaticChildren(__output) {
164
151
  _$_.pop_component();
165
152
  }
166
153
 
167
- export async function IfWithSiblingsAndChildren(__output, { children }) {
168
- return _$_.async(async () => {
169
- _$_.push_component();
154
+ export function IfWithSiblingsAndChildren(__output, { children }) {
155
+ _$_.push_component();
156
+
157
+ let lazy_2 = _$_.track(true);
170
158
 
171
- let lazy_2 = _$_.track(true);
159
+ __output.push('<section');
160
+ __output.push(' class="group"');
161
+ __output.push('>');
172
162
 
173
- __output.push('<section');
174
- __output.push(' class="group"');
163
+ {
164
+ __output.push('<div');
165
+ __output.push(' role="button"');
166
+ __output.push(' class="item"');
175
167
  __output.push('>');
176
168
 
177
169
  {
178
170
  __output.push('<div');
179
- __output.push(' role="button"');
180
- __output.push(' class="item"');
171
+ __output.push(' class="indicator"');
172
+ __output.push('>');
173
+ __output.push('</div>');
174
+ __output.push('<h2');
175
+ __output.push(' class="text"');
181
176
  __output.push('>');
182
177
 
183
178
  {
184
- __output.push('<div');
185
- __output.push(' class="indicator"');
186
- __output.push('>');
187
- __output.push('</div>');
188
- __output.push('<h2');
189
- __output.push(' class="text"');
190
- __output.push('>');
179
+ __output.push('Title');
180
+ }
191
181
 
192
- {
193
- __output.push('Title');
194
- }
182
+ __output.push('</h2>');
183
+ __output.push('<div');
184
+ __output.push(' class="caret"');
185
+ __output.push('>');
195
186
 
196
- __output.push('</h2>');
197
- __output.push('<div');
198
- __output.push(' class="caret"');
187
+ {
188
+ __output.push('<svg');
189
+ __output.push(' xmlns="http://www.w3.org/2000/svg"');
190
+ __output.push(' width="18"');
191
+ __output.push(' height="18"');
192
+ __output.push(' viewBox="0 0 24 24"');
199
193
  __output.push('>');
200
194
 
201
195
  {
202
- __output.push('<svg');
203
- __output.push(' xmlns="http://www.w3.org/2000/svg"');
204
- __output.push(' width="18"');
205
- __output.push(' height="18"');
206
- __output.push(' viewBox="0 0 24 24"');
196
+ __output.push('<path');
197
+ __output.push(' d="m9 18 6-6-6-6"');
207
198
  __output.push('>');
208
-
209
- {
210
- __output.push('<path');
211
- __output.push(' d="m9 18 6-6-6-6"');
212
- __output.push('>');
213
- __output.push('</path>');
214
- }
215
-
216
- __output.push('</svg>');
199
+ __output.push('</path>');
217
200
  }
218
201
 
219
- __output.push('</div>');
202
+ __output.push('</svg>');
220
203
  }
221
204
 
222
205
  __output.push('</div>');
223
- __output.push('<!--[-->');
206
+ }
224
207
 
225
- if (_$_.get(lazy_2)) {
226
- __output.push('<div');
227
- __output.push(' class="items"');
228
- __output.push('>');
208
+ __output.push('</div>');
209
+ __output.push('<!--[-->');
229
210
 
230
- {
231
- {
232
- const comp = children;
233
- const args = [__output, {}];
234
-
235
- if (comp?.async) {
236
- await comp(...args);
237
- } else if (comp) {
238
- comp(...args);
239
- }
240
- }
241
- }
211
+ if (_$_.get(lazy_2)) {
212
+ __output.push('<div');
213
+ __output.push(' class="items"');
214
+ __output.push('>');
242
215
 
243
- __output.push('</div>');
216
+ {
217
+ _$_.render_expression(__output, children);
244
218
  }
245
219
 
246
- __output.push('<!--]-->');
220
+ __output.push('</div>');
247
221
  }
248
222
 
249
- __output.push('</section>');
250
- _$_.pop_component();
251
- });
252
- }
223
+ __output.push('<!--]-->');
224
+ }
253
225
 
254
- IfWithSiblingsAndChildren.async = true;
226
+ __output.push('</section>');
227
+ _$_.pop_component();
228
+ }
255
229
 
256
230
  export function TestIfWithSiblingsAndChildren(__output) {
257
231
  _$_.push_component();
@@ -262,7 +236,7 @@ export function TestIfWithSiblingsAndChildren(__output) {
262
236
  const args = [
263
237
  __output,
264
238
  {
265
- children: function children(__output) {
239
+ children: _$_.ripple_element(function render_children(__output) {
266
240
  _$_.push_component();
267
241
 
268
242
  {
@@ -280,7 +254,7 @@ export function TestIfWithSiblingsAndChildren(__output) {
280
254
  }
281
255
 
282
256
  _$_.pop_component();
283
- }
257
+ })
284
258
  }
285
259
  ];
286
260
 
@@ -27,7 +27,7 @@ export async function SimplePortal(__output) {
27
27
  __output,
28
28
  {
29
29
  target: typeof document !== 'undefined' ? document.body : null,
30
- children: function children(__output) {
30
+ children: _$_.ripple_element(function render_children(__output) {
31
31
  _$_.push_component();
32
32
  __output.push('<div');
33
33
  __output.push(' class="portal-content"');
@@ -39,7 +39,7 @@ export async function SimplePortal(__output) {
39
39
 
40
40
  __output.push('</div>');
41
41
  _$_.pop_component();
42
- }
42
+ })
43
43
  }
44
44
  ];
45
45
 
@@ -88,7 +88,7 @@ export async function ConditionalPortal(__output) {
88
88
  __output,
89
89
  {
90
90
  target: typeof document !== 'undefined' ? document.body : null,
91
- children: function children(__output) {
91
+ children: _$_.ripple_element(function render_children(__output) {
92
92
  _$_.push_component();
93
93
  __output.push('<div');
94
94
  __output.push(' class="portal-content"');
@@ -100,7 +100,7 @@ export async function ConditionalPortal(__output) {
100
100
 
101
101
  __output.push('</div>');
102
102
  _$_.pop_component();
103
- }
103
+ })
104
104
  }
105
105
  ];
106
106
 
@@ -146,7 +146,7 @@ export async function PortalWithMainContent(__output) {
146
146
  __output,
147
147
  {
148
148
  target: typeof document !== 'undefined' ? document.body : null,
149
- children: function children(__output) {
149
+ children: _$_.ripple_element(function render_children(__output) {
150
150
  _$_.push_component();
151
151
  __output.push('<div');
152
152
  __output.push(' class="portal-content"');
@@ -158,7 +158,7 @@ export async function PortalWithMainContent(__output) {
158
158
 
159
159
  __output.push('</div>');
160
160
  _$_.pop_component();
161
- }
161
+ })
162
162
  }
163
163
  ];
164
164
 
@@ -219,7 +219,7 @@ export async function NestedContentWithPortal(__output) {
219
219
  __output,
220
220
  {
221
221
  target: typeof document !== 'undefined' ? document.body : null,
222
- children: function children(__output) {
222
+ children: _$_.ripple_element(function render_children(__output) {
223
223
  _$_.push_component();
224
224
  __output.push('<div');
225
225
  __output.push(' class="portal-content"');
@@ -231,7 +231,7 @@ export async function NestedContentWithPortal(__output) {
231
231
 
232
232
  __output.push('</div>');
233
233
  _$_.pop_component();
234
- }
234
+ })
235
235
  }
236
236
  ];
237
237
 
@@ -1,4 +1,5 @@
1
1
  // Basic static components for hydration testing
2
+ import { track } from 'ripple';
2
3
  import type { Children } from 'ripple';
3
4
 
4
5
  export component StaticText() {
@@ -60,9 +61,20 @@ export component WithGreeting() {
60
61
 
61
62
  export component ExpressionContent() {
62
63
  const value = 42;
63
- const text = 'computed';
64
+ const label = 'computed';
64
65
  <div>{value}</div>
65
- <span>{text.toUpperCase()}</span>
66
+ <span>{label.toUpperCase()}</span>
67
+ }
68
+
69
+ component TextProp(&{ children }: { children: string }) {
70
+ <div class="text-prop">{children}</div>
71
+ }
72
+
73
+ export component TextPropWithToggle() {
74
+ let &[show] = track(false);
75
+
76
+ <TextProp children={show ? 'hello' : ''} />
77
+ <button class="show-text" onClick={() => (show = true)}>{'Show'}</button>
66
78
  }
67
79
 
68
80
  // Test for static content in child component followed by sibling content
@@ -99,9 +111,7 @@ component Actions({ playgroundVisible = false }: { playgroundVisible: boolean })
99
111
 
100
112
  component Layout({ children }: { children: Children }) {
101
113
  <main>
102
- <div class="container">
103
- <children />
104
- </div>
114
+ <div class="container">{children}</div>
105
115
  </main>
106
116
  }
107
117
 
@@ -1,8 +1,14 @@
1
1
  import type { Children } from 'ripple';
2
2
 
3
3
  export component Layout(&{ children }: { children?: Children }) {
4
+ <div class="layout">{children}</div>
5
+ }
6
+
7
+ export component TextWrappedLayout(&{ children }: { children?: Children }) {
4
8
  <div class="layout">
5
- <children />
9
+ {text 'before'}
10
+ {children}
11
+ {text 'after'}
6
12
  </div>
7
13
  }
8
14
 
@@ -37,3 +43,9 @@ export component LayoutWithMultiRootChild() {
37
43
  <MultiRootChild />
38
44
  </Layout>
39
45
  }
46
+
47
+ export component LayoutWithTextAroundChildren() {
48
+ <TextWrappedLayout>
49
+ <SingleChild />
50
+ </TextWrappedLayout>
51
+ }
@@ -11,9 +11,7 @@ import { track } from 'ripple';
11
11
  export component Layout({ children }: { children: any }) {
12
12
  <div class="layout">
13
13
  <nav class="nav">{'Navigation'}</nav>
14
- <main class="main">
15
- <children />
16
- </main>
14
+ <main class="main">{children}</main>
17
15
  </div>
18
16
  }
19
17
 
@@ -38,9 +38,7 @@ export component HtmlWithReactivity() {
38
38
 
39
39
  export component HtmlWrapper({ children }: { children: any }) {
40
40
  <div class="wrapper">
41
- <div class="inner">
42
- <children />
43
- </div>
41
+ <div class="inner">{children}</div>
44
42
  </div>
45
43
  }
46
44
 
@@ -105,9 +103,7 @@ export component DocLayout({
105
103
  <div class="layout">
106
104
  <div class="content-container">
107
105
  <article>
108
- <div>
109
- <children />
110
- </div>
106
+ <div>{children}</div>
111
107
  </article>
112
108
  if (editPath) {
113
109
  <div class="edit-link">
@@ -168,14 +164,10 @@ export component HtmlWithUndefinedContent() {
168
164
  component DynamicHeading({ level, children }: { level: number; children: any }) {
169
165
  switch (level) {
170
166
  case 1: {
171
- <h1 class="heading">
172
- <children />
173
- </h1>
167
+ <h1 class="heading">{children}</h1>
174
168
  }
175
169
  case 2: {
176
- <h2 class="heading">
177
- <children />
178
- </h2>
170
+ <h2 class="heading">{children}</h2>
179
171
  }
180
172
  }
181
173
  }
@@ -193,9 +185,7 @@ component CodeBlock({ code }: { code: string }) {
193
185
 
194
186
  component ContentWrapper({ children }: { children: any }) {
195
187
  <div class="wrapper">
196
- <div class="inner">
197
- <children />
198
- </div>
188
+ <div class="inner">{children}</div>
199
189
  </div>
200
190
  }
201
191
 
@@ -211,7 +201,7 @@ export component HtmlAfterSwitchInChildren() {
211
201
 
212
202
  component NavItem({
213
203
  href,
214
- text,
204
+ text: label,
215
205
  active = false,
216
206
  }: {
217
207
  href: string;
@@ -223,7 +213,7 @@ component NavItem({
223
213
  <div class="indicator" />
224
214
  }
225
215
  <a {href}>
226
- <span>{text}</span>
216
+ <span>{label}</span>
227
217
  </a>
228
218
  </div>
229
219
  }
@@ -236,9 +226,7 @@ component SidebarSection({ title, children }: { title: string; children: any })
236
226
  <button onClick={() => (expanded = !expanded)}>{'Toggle'}</button>
237
227
  </div>
238
228
  if (expanded) {
239
- <div class="section-items">
240
- <children />
241
- </div>
229
+ <div class="section-items">{children}</div>
242
230
  }
243
231
  </section>
244
232
  }
@@ -293,9 +281,7 @@ export component LayoutWithSidebarAndMain() {
293
281
 
294
282
  component ArticleWrapper({ children }: { children: any }) {
295
283
  <article class="doc-content">
296
- <div>
297
- <children />
298
- </div>
284
+ <div>{children}</div>
299
285
  </article>
300
286
  }
301
287
 
@@ -341,9 +327,7 @@ export component ArticleWithHtmlChildThenSibling() {
341
327
  component InlineArticleLayout({ children }: { children: any }) {
342
328
  <div class="content-container">
343
329
  <article class="doc-content">
344
- <div>
345
- <children />
346
- </div>
330
+ <div>{children}</div>
347
331
  </article>
348
332
  if (true) {
349
333
  <div class="edit-link">
@@ -391,9 +375,7 @@ component DocsLayoutInner({
391
375
  <div class="content">
392
376
  <div class="content-container">
393
377
  <article class="doc-content">
394
- <div>
395
- <children />
396
- </div>
378
+ <div>{children}</div>
397
379
  </article>
398
380
  if (editPath) {
399
381
  <div class="edit-link">
@@ -450,9 +432,7 @@ component DocsLayoutExact({
450
432
  <div class="content">
451
433
  <div class="content-container">
452
434
  <article class="doc-content">
453
- <div>
454
- <children />
455
- </div>
435
+ <div>{children}</div>
456
436
  </article>
457
437
  if (editPath) {
458
438
  <div class="edit-link">
@@ -542,9 +522,7 @@ export component TemplateWithHtmlAndSiblings() {
542
522
  component LayoutWithTemplate({ children, data }: { children: any; data: object }) {
543
523
  <div class="layout">
544
524
  <template id="page-data">{html JSON.stringify(data)}</template>
545
- <main>
546
- <children />
547
- </main>
525
+ <main>{children}</main>
548
526
  </div>
549
527
  }
550
528
 
@@ -9,15 +9,13 @@ export component IfWithChildren({ children }: { children: any }) {
9
9
  <div class="container">
10
10
  <div class="header" role="button" onClick={() => (expanded = !expanded)}>{'Toggle'}</div>
11
11
  if (expanded) {
12
- <div class="content">
13
- <children />
14
- </div>
12
+ <div class="content">{children}</div>
15
13
  }
16
14
  </div>
17
15
  }
18
16
 
19
- export component ChildItem({ text }: { text: string }) {
20
- <div class="item">{text}</div>
17
+ export component ChildItem({ text: label }: { text: string }) {
18
+ <div class="item">{label}</div>
21
19
  }
22
20
 
23
21
  export component TestIfWithChildren() {
@@ -57,9 +55,7 @@ export component IfWithSiblingsAndChildren({ children }: { children: any }) {
57
55
  </div>
58
56
  </div>
59
57
  if (expanded) {
60
- <div class="items">
61
- <children />
62
- </div>
58
+ <div class="items">{children}</div>
63
59
  }
64
60
  </section>
65
61
  }
@@ -39,4 +39,15 @@ describe('hydration > composite', () => {
39
39
  '<div class=\"layout\"><h1>title</h1><p>description</p></div>',
40
40
  );
41
41
  });
42
+
43
+ it('hydrates explicit text around children', async () => {
44
+ await hydrateComponent(
45
+ ServerComponents.LayoutWithTextAroundChildren,
46
+ ClientComponents.LayoutWithTextAroundChildren,
47
+ );
48
+ expect(container.innerHTML).toBeHtml(
49
+ '<div class="layout">before<div class="single">single</div>after</div>',
50
+ );
51
+ expect(container.querySelector('.layout')?.textContent).toBe('beforesingleafter');
52
+ });
42
53
  });
@@ -345,6 +345,56 @@ describe('basic server > attribute rendering', () => {
345
345
  expect(body).toBeHtml('<input type="checkbox" disabled checked />');
346
346
  });
347
347
 
348
+ it('renders formnovalidate as a boolean attribute', async () => {
349
+ component Basic() {
350
+ let &[formnovalidate] = track(true);
351
+
352
+ <button {formnovalidate}>{'Submit'}</button>
353
+ }
354
+
355
+ const { body } = await render(Basic);
356
+ const { document } = parseHtml(body);
357
+
358
+ const button = document.querySelector('button');
359
+
360
+ expect(button.hasAttribute('formnovalidate')).toBe(true);
361
+ expect(button.getAttribute('formnovalidate')).toBe('');
362
+ expect(body).toBeHtml('<button formnovalidate>Submit</button>');
363
+ });
364
+
365
+ it('renders hidden as a boolean attribute when true', async () => {
366
+ component Basic() {
367
+ let &[hidden] = track(true);
368
+
369
+ <div {hidden}>{'Hidden content'}</div>
370
+ }
371
+
372
+ const { body } = await render(Basic);
373
+ const { document } = parseHtml(body);
374
+
375
+ const div = document.querySelector('div');
376
+
377
+ expect(div.hasAttribute('hidden')).toBe(true);
378
+ expect(div.getAttribute('hidden')).toBe('');
379
+ expect(body).toBeHtml('<div hidden>Hidden content</div>');
380
+ });
381
+
382
+ it('does not render hidden when false', async () => {
383
+ component Basic() {
384
+ let &[hidden] = track(false);
385
+
386
+ <div {hidden}>{'Visible content'}</div>
387
+ }
388
+
389
+ const { body } = await render(Basic);
390
+ const { document } = parseHtml(body);
391
+
392
+ const div = document.querySelector('div');
393
+
394
+ expect(div.hasAttribute('hidden')).toBe(false);
395
+ expect(body).toBeHtml('<div>Visible content</div>');
396
+ });
397
+
348
398
  it('render multiple dynamic attributes', async () => {
349
399
  component Basic() {
350
400
  let &[theme] = track('light');