ripple 0.2.115 → 0.2.118

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 (93) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/1-parse/index.js +79 -0
  4. package/src/compiler/phases/3-transform/client/index.js +54 -8
  5. package/src/compiler/phases/3-transform/segments.js +107 -60
  6. package/src/compiler/phases/3-transform/server/index.js +21 -11
  7. package/src/compiler/types/index.d.ts +16 -0
  8. package/src/runtime/index-client.js +19 -185
  9. package/src/runtime/index-server.js +24 -0
  10. package/src/runtime/internal/client/bindings.js +443 -0
  11. package/src/runtime/internal/client/index.js +4 -0
  12. package/src/runtime/internal/client/runtime.js +10 -0
  13. package/src/runtime/internal/client/utils.js +0 -8
  14. package/src/runtime/map.js +11 -1
  15. package/src/runtime/set.js +11 -1
  16. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  17. package/tests/client/_etc.test.ripple +5 -0
  18. package/tests/client/array/array.copy-within.test.ripple +120 -0
  19. package/tests/client/array/array.derived.test.ripple +495 -0
  20. package/tests/client/array/array.iteration.test.ripple +115 -0
  21. package/tests/client/array/array.mutations.test.ripple +385 -0
  22. package/tests/client/array/array.static.test.ripple +237 -0
  23. package/tests/client/array/array.to-methods.test.ripple +93 -0
  24. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  25. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  26. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  27. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  28. package/tests/client/basic/basic.collections.test.ripple +94 -0
  29. package/tests/client/basic/basic.components.test.ripple +225 -0
  30. package/tests/client/basic/basic.errors.test.ripple +126 -0
  31. package/tests/client/basic/basic.events.test.ripple +222 -0
  32. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  33. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  34. package/tests/client/basic/basic.styling.test.ripple +63 -0
  35. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  36. package/tests/client/boundaries.test.ripple +2 -21
  37. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  38. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  39. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  40. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  41. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  42. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  43. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  44. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  45. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  46. package/tests/client/composite/composite.generics.test.ripple +211 -0
  47. package/tests/client/composite/composite.props.test.ripple +106 -0
  48. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  49. package/tests/client/composite/composite.render.test.ripple +84 -0
  50. package/tests/client/computed-properties.test.ripple +2 -21
  51. package/tests/client/context.test.ripple +5 -22
  52. package/tests/client/date.test.ripple +1 -20
  53. package/tests/client/dynamic-elements.test.ripple +16 -24
  54. package/tests/client/for.test.ripple +4 -23
  55. package/tests/client/head.test.ripple +11 -23
  56. package/tests/client/html.test.ripple +1 -20
  57. package/tests/client/input-value.test.ripple +11 -31
  58. package/tests/client/map.test.ripple +82 -20
  59. package/tests/client/media-query.test.ripple +10 -23
  60. package/tests/client/object.test.ripple +5 -24
  61. package/tests/client/portal.test.ripple +2 -19
  62. package/tests/client/ref.test.ripple +8 -26
  63. package/tests/client/set.test.ripple +84 -22
  64. package/tests/client/svg.test.ripple +1 -22
  65. package/tests/client/switch.test.ripple +6 -25
  66. package/tests/client/tracked-expression.test.ripple +2 -21
  67. package/tests/client/typescript-generics.test.ripple +0 -21
  68. package/tests/client/url/url.derived.test.ripple +83 -0
  69. package/tests/client/url/url.parsing.test.ripple +165 -0
  70. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  71. package/tests/client/url/url.reactivity.test.ripple +449 -0
  72. package/tests/client/url/url.serialization.test.ripple +50 -0
  73. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  74. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  75. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  76. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  77. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  78. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  79. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  80. package/tests/client.d.ts +12 -0
  81. package/tests/server/if.test.ripple +66 -0
  82. package/tests/setup-client.js +28 -0
  83. package/tsconfig.json +4 -2
  84. package/types/index.d.ts +92 -46
  85. package/LICENSE +0 -21
  86. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  87. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  88. package/tests/client/array.test.ripple +0 -1455
  89. package/tests/client/basic.test.ripple +0 -1892
  90. package/tests/client/compiler.test.ripple +0 -541
  91. package/tests/client/composite.test.ripple +0 -692
  92. package/tests/client/url-search-params.test.ripple +0 -912
  93. package/tests/client/url.test.ripple +0 -954
@@ -1,6 +1,6 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`composite components > correct handles passing through component props and children 1`] = `
3
+ exports[`composite > render > correct handles passing through component props and children 1`] = `
4
4
  <div>
5
5
  <!---->
6
6
  <div>
@@ -22,7 +22,7 @@ exports[`composite components > correct handles passing through component props
22
22
  </div>
23
23
  `;
24
24
 
25
- exports[`composite components > render simple text as children 1`] = `
25
+ exports[`composite > render > render simple text as children 1`] = `
26
26
  <div>
27
27
  <!---->
28
28
  <button
@@ -0,0 +1,100 @@
1
+ import { track, flushSync } from 'ripple';
2
+
3
+ describe('composite > dynamic components', () => {
4
+ it('supports rendering compositie components using <@component> syntax', () => {
5
+ component App() {
6
+ component basic() {
7
+ <div>{'Basic Component'}</div>
8
+ }
9
+
10
+ const tracked_basic = track(() => basic);
11
+
12
+ <@tracked_basic />
13
+ }
14
+
15
+ render(App);
16
+ flushSync();
17
+
18
+ expect(container.textContent).toBe('Basic Component');
19
+ });
20
+
21
+ it('supports rendering composite components using <object.@component> syntax', () => {
22
+ component App() {
23
+ component basic() {
24
+ <div>{'Basic Component'}</div>
25
+ }
26
+
27
+ const tracked_basic = track(() => basic);
28
+
29
+ const obj = {
30
+ tracked_basic,
31
+ }
32
+
33
+ <obj.@tracked_basic />
34
+ }
35
+
36
+ render(App);
37
+ flushSync();
38
+
39
+ expect(container.textContent).toBe('Basic Component');
40
+ });
41
+
42
+ it('supports rendering composite components using <@object.@component> syntax', () => {
43
+ component App() {
44
+ component basic() {
45
+ <div>{'Basic Component'}</div>
46
+ }
47
+
48
+ const tracked_basic = track(() => basic);
49
+
50
+ const obj = {
51
+ tracked_basic,
52
+ }
53
+
54
+ const tracked_object = track(obj);
55
+
56
+ <@tracked_object.@tracked_basic />
57
+ }
58
+
59
+ render(App);
60
+ flushSync();
61
+
62
+ expect(container.textContent).toBe('Basic Component');
63
+ });
64
+
65
+ it('handles dynamic component switching', () => {
66
+ component Child1() {
67
+ <div>{"I am child 1"}</div>
68
+ }
69
+
70
+ component Child2() {
71
+ <div>{"I am child 2"}</div>
72
+ }
73
+
74
+ component App() {
75
+ let thing = track(() => Child1)
76
+
77
+ <div id="container">
78
+ <@thing />
79
+ </div>
80
+
81
+ <button onClick={() => @thing = @thing === Child1 ? Child2 : Child1}>{"Change Child"}</button>
82
+ }
83
+
84
+ render(App);
85
+ flushSync();
86
+
87
+ expect(container.querySelector('#container').textContent).toBe('I am child 1');
88
+
89
+ const button = container.querySelector('button');
90
+ button.click();
91
+ flushSync();
92
+
93
+ expect(container.querySelector('#container').textContent).toBe('I am child 2');
94
+
95
+ button.click();
96
+ flushSync();
97
+
98
+ expect(container.querySelector('#container').textContent).toBe('I am child 1');
99
+ });
100
+ });
@@ -0,0 +1,211 @@
1
+ import { TrackedArray, TrackedMap } from 'ripple';
2
+
3
+ describe('composite > generics', () => {
4
+ it('handles advanced generic ambiguity and edge cases', () => {
5
+ component App() {
6
+ // Ambiguous generics vs JSX / less-than parsing scenarios
7
+
8
+ // 7. Generic following optional chaining
9
+ const maybe = {
10
+ factory<T>() {
11
+ return {
12
+ make<U>() {
13
+ return 1;
14
+ }
15
+ };
16
+ }
17
+ };
18
+ const g = maybe?.factory<number>()?.make<boolean>();
19
+
20
+ // 8. Comparison operator (ensure '<' here NOT misparsed as generics)
21
+ let x = 10, y = 20;
22
+ const h = x < y ? 'lt' : 'ge';
23
+
24
+ // 9. Chained comparisons with intervening generics
25
+ class Box<T> {
26
+ value: T;
27
+ constructor(value?: T) {
28
+ this.value = value;
29
+ }
30
+ open<U>() {
31
+ return new Box<U>();
32
+ }
33
+ }
34
+ const limit = 100;
35
+ const i = new Box<number>().value < limit ? 'ok' : 'no';
36
+
37
+ // 10. JSX / Element should still work
38
+ <div class="still-works">
39
+ <span>{'Test'}</span>
40
+ </div>
41
+
42
+ // 11. Generic function call vs Element: Identifier followed by generic args
43
+ function identity<T>(value: T): T {
44
+ return value;
45
+ }
46
+ const j = identity<number>(42);
47
+
48
+ // 12. Member + generic call immediately followed by another call
49
+ class Factory {
50
+ create<T>() {
51
+ return (value: T) => value;
52
+ }
53
+ }
54
+ const factory = new Factory();
55
+ const k = factory.create<number>()(123);
56
+
57
+ // 13. Multiple generic segments in chain
58
+ function foo<T>() {
59
+ return {
60
+ bar<U>() {
61
+ return {
62
+ baz<V>() {
63
+ return true;
64
+ }
65
+ };
66
+ }
67
+ };
68
+ }
69
+ const l = foo<number>().bar<string>().baz<boolean>();
70
+
71
+ // 14. Generic with constraint + default
72
+ type Extractor<T extends { id: number } = { id: number }> = (v: T) => number;
73
+ const m: Extractor = (v) => v.id;
74
+
75
+ // 15. Generic in angle after "new" + trailing call
76
+ class Wrapper<T> {
77
+ value: T;
78
+ constructor() {
79
+ this.value = null as unknown as T;
80
+ }
81
+ unwrap<U>() {
82
+ return null as unknown as U;
83
+ }
84
+ }
85
+ const n = new Wrapper<number>().unwrap<string>();
86
+
87
+ // 16. Angle brackets inside type assertion vs generic call
88
+ function getUnknown(): unknown {
89
+ return new Map<string, number>([['a', 1]]);
90
+ }
91
+ getUnknown.factory = function<T>() {
92
+ return {
93
+ make<U>() {
94
+ return 2;
95
+ }
96
+ };
97
+ };
98
+ const raw = getUnknown();
99
+ const o = (raw as Map<string, number>).get('a');
100
+
101
+ // 17. Generic with comma + trailing less-than comparison on next token
102
+ class Pair<T1, T2> {
103
+ first: T1;
104
+ second: T2;
105
+ constructor() {
106
+ this.first = null as unknown as T1;
107
+ this.second = null as unknown as T2;
108
+ }
109
+ }
110
+ const p = new Pair<number, string>();
111
+ const q = 1 < 2 ? p : null;
112
+
113
+ // 18. Nested generics with line breaks resembling JSX indentation
114
+ interface Node<T> {
115
+ value: T;
116
+ }
117
+ interface Edge<W> {
118
+ weight: W;
119
+ }
120
+ class Graph<N, E> {
121
+ nodes: N[];
122
+ edges: E[];
123
+ constructor() {
124
+ this.nodes = [];
125
+ this.edges = [];
126
+ }
127
+ }
128
+ const r = new Graph<
129
+ Node<string>,
130
+ Edge<number>
131
+ >();
132
+
133
+ // 19. Ternary containing generics in both branches
134
+ let flag = true;
135
+ const s = flag ? new Box<number>() : new Box<string>();
136
+
137
+ // 20. Generic inside template expression
138
+ const t = `length=${new TrackedArray<number>().length}`;
139
+
140
+ // 21. Optional chaining + generic + property access
141
+ const registry = new TrackedMap<string, number>();
142
+ const u = registry.get<number>('id')?.toString();
143
+
144
+ // 22. Generic call used as callee for another call
145
+ function make<T>() {
146
+ return (value: T) => value;
147
+ }
148
+ const v = make<number>()(10);
149
+
150
+ // 23. Generic followed by tagged template (ensure not confused with JSX)
151
+ function tagFn<T>(strings: TemplateStringsArray, ...values: T[]) {
152
+ return values[0];
153
+ }
154
+ const tagResult = tagFn<number>`value`;
155
+
156
+ // 24. Sequence mixing: (a < b) + generic call in same statement
157
+ function compute<T>(x: T, y: T): T {
158
+ return y;
159
+ }
160
+
161
+ const w = (x < y) && compute<number>(x, y);
162
+
163
+
164
+ // Additional component focusing on edge crankers
165
+
166
+ // 28. Generic after parenthesized new expression
167
+ const aa = (new Box<number>()).open<string>();
168
+
169
+ // 29. Generic chain right after closing paren of IIFE
170
+ class Builder<Kind> {
171
+ finalize<Result>() {
172
+ return {
173
+ result: null as unknown as Result
174
+ };
175
+ }
176
+ }
177
+ const builder = new Builder<Number>();
178
+ const result = ((function(){ return builder; })() as Builder<Number>).finalize<boolean>();
179
+
180
+ // 30. Angle bracket start of conditional expression line
181
+ function adjust<T>(value: T): T {
182
+ return value;
183
+ }
184
+ const val =
185
+ new Wrapper<number>()
186
+ .value < 100
187
+ ? adjust<number>(10)
188
+ : adjust<number>(20);
189
+
190
+
191
+ // 32. Generic with comments inside angle list
192
+ class Mapper<Key, Value> {
193
+ map: Map<Key, Value>;
194
+ constructor() {
195
+ this.map = new Map<Key, Value>();
196
+ }
197
+ }
198
+ const gg = new Mapper<
199
+ // key type
200
+ string,
201
+ /* value type */
202
+ number
203
+ >();
204
+
205
+ // 33. Map of generic instance as key
206
+ const mm = new Map<TrackedArray<number>, TrackedArray<string>>();
207
+ }
208
+
209
+ render(App);
210
+ });
211
+ });
@@ -0,0 +1,106 @@
1
+ import { track, effect, flushSync } from 'ripple';
2
+
3
+ describe('composite > props', () => {
4
+ it('correctly handles default prop values', () => {
5
+ component Child({ foo = 456 }) {
6
+ <div>{foo}</div>
7
+ }
8
+
9
+ component App() {
10
+ let foo = track(123);
11
+
12
+ <Child />
13
+ <Child {@foo} />
14
+ }
15
+
16
+ render(App);
17
+
18
+ expect(container.querySelectorAll('div')[0].textContent).toBe('456');
19
+ expect(container.querySelectorAll('div')[1].textContent).toBe('123');
20
+ });
21
+
22
+ it('correctly handles default prop values #2', () => {
23
+ component Child({ foo = 456 }) {
24
+ <div>{foo}</div>
25
+ }
26
+
27
+ component App() {
28
+ let foo = 123;
29
+
30
+ <Child />
31
+ <Child {foo} />
32
+ }
33
+
34
+ render(App);
35
+
36
+ expect(container.querySelectorAll('div')[0].textContent).toBe('456');
37
+ expect(container.querySelectorAll('div')[1].textContent).toBe('123');
38
+ });
39
+
40
+ it('correctly handles no props', () => {
41
+ component Child(props: { foo?: Tracked<number> }) {
42
+ <div>{props.@foo}</div>
43
+ }
44
+
45
+ component App() {
46
+ let foo = track(123);
47
+
48
+ <Child />
49
+ <Child {foo} />
50
+ }
51
+
52
+ render(App);
53
+
54
+ expect(container.querySelectorAll('div')[0].textContent).toBe('');
55
+ expect(container.querySelectorAll('div')[1].textContent).toBe('123');
56
+ });
57
+
58
+ it('correctly handles no props #2', () => {
59
+ component Child({ foo }) {
60
+ <div>{foo}</div>
61
+ }
62
+
63
+ component App() {
64
+ let foo = track(123);
65
+
66
+ <Child />
67
+ <Child {@foo} />
68
+ }
69
+
70
+ render(App);
71
+
72
+ expect(container.querySelectorAll('div')[0].textContent).toBe('');
73
+ expect(container.querySelectorAll('div')[1].textContent).toBe('123');
74
+ });
75
+
76
+ it('mutating a tracked value prop should work as intended', () => {
77
+ const logs: number[] = [];
78
+
79
+ component Counter({count}) {
80
+ effect(() => {
81
+ logs.push(@count);
82
+ });
83
+
84
+ <button onClick={() => @count = @count + 1}>{'+'}</button>
85
+ }
86
+
87
+ component App() {
88
+ const count = track(0);
89
+
90
+ <div>
91
+ <Counter count={count} />
92
+ </div>
93
+ }
94
+
95
+ render(App);
96
+ flushSync();
97
+
98
+ expect(logs).toEqual([0]);
99
+
100
+ const button = container.querySelector('button');
101
+ button.click();
102
+ flushSync();
103
+
104
+ expect(logs).toEqual([0, 1]);
105
+ })
106
+ });
@@ -0,0 +1,184 @@
1
+ import { track, flushSync, effect } from 'ripple';
2
+
3
+ describe('composite > reactivity', () => {
4
+ it('renders composite components with object state', () => {
5
+ component Button({ obj }: { obj: { count: Tracked<number> } }) {
6
+ <button class='count2' onClick={() => {
7
+ obj.@count++;
8
+ }}>{obj.@count}</button>
9
+ }
10
+
11
+ component App() {
12
+ <div>
13
+ let obj = {
14
+ count: track(0)
15
+ };
16
+
17
+ <span class='count'>{obj.@count}</span>
18
+ <Button obj={obj} />
19
+ </div>
20
+ }
21
+
22
+ render(App);
23
+
24
+ const button = container.querySelector('button');
25
+
26
+ button.click();
27
+ flushSync();
28
+
29
+ expect(container.querySelector('.count').textContent).toBe('1');
30
+ expect(container.querySelector('.count2').textContent).toBe('1');
31
+ });
32
+
33
+ it('renders composite components with object state wrapped in an if statement', () => {
34
+ component Button({ obj }: { obj: { count: Tracked<number> } }) {
35
+ <button class='count2' onClick={() => {
36
+ obj.@count++;
37
+ }}>{obj.@count}</button>
38
+ }
39
+
40
+ component OtherComponent({ obj }: { obj: { count: Tracked<number> } }) {
41
+ <div class='count3'>{obj.@count}</div>
42
+ }
43
+
44
+ component App() {
45
+ <div>
46
+ let obj = {
47
+ count: track(0)
48
+ };
49
+
50
+ <span class='count'>{obj.@count}</span>
51
+ <span>{' '}</span>
52
+ if (obj) {
53
+ <Button obj={obj} />
54
+ }
55
+
56
+ if (obj) {
57
+ <OtherComponent obj={obj} />
58
+ }
59
+ </div>
60
+ }
61
+
62
+ render(App);
63
+
64
+ const button = container.querySelector('button');
65
+
66
+ button.click();
67
+ flushSync();
68
+
69
+ expect(container.querySelector('.count').textContent).toBe('1');
70
+ expect(container.querySelector('.count2').textContent).toBe('1');
71
+ expect(container.querySelector('.count3').textContent).toBe('1');
72
+ });
73
+
74
+ it('parents and children have isolated state', () => {
75
+ component Button(props: { count: number }) {
76
+ let count = track(() => props.count);
77
+ <button onClick={() => { @count++; } }>{"child: " + @count}</button>
78
+ }
79
+
80
+ component App() {
81
+ <div>
82
+ let count = track(0);
83
+
84
+ <button onClick={() => { @count++; } }>{"parent: " + @count}</button>
85
+ <Button {@count} />
86
+ </div>
87
+ }
88
+
89
+ render(App);
90
+
91
+ const buttons = container.querySelectorAll('button');
92
+
93
+ expect(buttons[0].textContent).toBe('parent: 0');
94
+ expect(buttons[1].textContent).toBe('child: 0');
95
+
96
+ buttons[0].click();
97
+ flushSync();
98
+
99
+ expect(buttons[0].textContent).toBe('parent: 1');
100
+ expect(buttons[1].textContent).toBe('child: 1');
101
+
102
+ buttons[1].click();
103
+ flushSync();
104
+
105
+ expect(buttons[0].textContent).toBe('parent: 1');
106
+ expect(buttons[1].textContent).toBe('child: 2');
107
+ });
108
+
109
+ it('parents and children have isolated connected state (destructured props)', () => {
110
+ component Button({count}: { count: number }) {
111
+ let local_count = track(() => count);
112
+ <button onClick={() => { @local_count++; } }>{"child: " + @local_count}</button>
113
+ }
114
+
115
+ component App() {
116
+ <div>
117
+ let count = track(0);
118
+
119
+ <button onClick={() => { @count++; } }>{"parent: " + @count}</button>
120
+ <Button {@count} />
121
+ </div>
122
+ }
123
+
124
+ render(App);
125
+
126
+ const buttons = container.querySelectorAll('button');
127
+
128
+ expect(buttons[0].textContent).toBe('parent: 0');
129
+ expect(buttons[1].textContent).toBe('child: 0');
130
+
131
+ buttons[0].click();
132
+ flushSync();
133
+
134
+ expect(buttons[0].textContent).toBe('parent: 1');
135
+ expect(buttons[1].textContent).toBe('child: 1');
136
+
137
+ buttons[1].click();
138
+ flushSync();
139
+
140
+ expect(buttons[0].textContent).toBe('parent: 1');
141
+ expect(buttons[1].textContent).toBe('child: 2');
142
+ });
143
+
144
+ it('handles spreading of props', () => {
145
+ let logs: string[] = [];
146
+
147
+ component App() {
148
+ const a = track(1);
149
+ const b = track(2);
150
+ const c = track(3);
151
+
152
+ const obj = track(() => ({
153
+ @a,
154
+ @b,
155
+ @c,
156
+ }));
157
+
158
+ <Child {...@obj} />
159
+
160
+ <button onClick={() => { @a++; @b++; @c++; }}>{"Increment all"}</button>
161
+ }
162
+
163
+ component Child({ a, b, c }: { a: number; b: number; c: number }) {
164
+ effect(() => {
165
+ logs.push(`Child effect: ${a}, ${b}, ${c}`);
166
+ });
167
+
168
+ <div>{a + ' ' + b + ' ' + c}</div>
169
+ }
170
+
171
+ render(App);
172
+ flushSync();
173
+
174
+ expect(container.querySelector('div').textContent).toBe('1 2 3');
175
+ expect(logs).toEqual(['Child effect: 1, 2, 3']);
176
+
177
+ const button = container.querySelector('button');
178
+ button.click();
179
+ flushSync();
180
+
181
+ expect(container.querySelector('div').textContent).toBe('2 3 4');
182
+ expect(logs).toEqual(['Child effect: 1, 2, 3', 'Child effect: 2, 3, 4']);
183
+ });
184
+ });
@@ -0,0 +1,84 @@
1
+ import { track, flushSync, TrackedArray } from 'ripple';
2
+
3
+ describe('composite > render', () => {
4
+ it('renders composite components', () => {
5
+ component Button({ count }: { count: number }) {
6
+ <div>{count}</div>
7
+ }
8
+
9
+ component App() {
10
+ let count = track(0);
11
+
12
+ <button onClick={() => @count++}>{'Increment'}</button>
13
+ <Button {@count} />
14
+ }
15
+
16
+ render(App);
17
+
18
+ const button = container.querySelector('button');
19
+
20
+ button.click();
21
+ flushSync();
22
+
23
+ expect(container.querySelector('div').textContent).toBe('1');
24
+
25
+ button.click();
26
+ flushSync();
27
+
28
+ expect(container.querySelector('div').textContent).toBe('2');
29
+ });
30
+
31
+ it('correct handles passing through component props and children', () => {
32
+ component Button({ A, B, children }: { A: () => void; B: () => void; children: () => void }) {
33
+ <div>
34
+ <A />
35
+ <children />
36
+ <B />
37
+ </div>
38
+ }
39
+
40
+ component App() {
41
+ <Button>
42
+ component A() {
43
+ <div>{"I am A"}</div>
44
+ }
45
+ <div>{"other text"}</div>
46
+ component B() {
47
+ <div>{"I am B"}</div>
48
+ }
49
+ </Button>
50
+ }
51
+
52
+ render(App);
53
+
54
+ expect(container).toMatchSnapshot();
55
+ });
56
+
57
+ it('render simple text as children', () => {
58
+ component App() {
59
+ let name = 'Click Me';
60
+
61
+ <Child
62
+ class="my-button"
63
+ >{name}</Child>;
64
+ }
65
+
66
+ component Child({children, ...rest}: {children: string; class: string}) {
67
+ <button {...rest}><children /></button>
68
+ }
69
+
70
+ render(App);
71
+ expect(container).toMatchSnapshot();
72
+ });
73
+
74
+ it('handles generics', () => {
75
+ component ArrayTest() {
76
+ let items = new TrackedArray<number>();
77
+ items.push.apply(items, [1, 2, 3, 4, 5]);
78
+
79
+ <pre>{items ? JSON.stringify(items) : 'Loading...'}</pre>
80
+ }
81
+
82
+ render(ArrayTest);
83
+ });
84
+ });