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.
- package/package.json +16 -16
- package/src/compiler/index.js +20 -1
- package/src/compiler/phases/1-parse/index.js +79 -0
- package/src/compiler/phases/3-transform/client/index.js +54 -8
- package/src/compiler/phases/3-transform/segments.js +107 -60
- package/src/compiler/phases/3-transform/server/index.js +21 -11
- package/src/compiler/types/index.d.ts +16 -0
- package/src/runtime/index-client.js +19 -185
- package/src/runtime/index-server.js +24 -0
- package/src/runtime/internal/client/bindings.js +443 -0
- package/src/runtime/internal/client/index.js +4 -0
- package/src/runtime/internal/client/runtime.js +10 -0
- package/src/runtime/internal/client/utils.js +0 -8
- package/src/runtime/map.js +11 -1
- package/src/runtime/set.js +11 -1
- package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
- package/tests/client/_etc.test.ripple +5 -0
- package/tests/client/array/array.copy-within.test.ripple +120 -0
- package/tests/client/array/array.derived.test.ripple +495 -0
- package/tests/client/array/array.iteration.test.ripple +115 -0
- package/tests/client/array/array.mutations.test.ripple +385 -0
- package/tests/client/array/array.static.test.ripple +237 -0
- package/tests/client/array/array.to-methods.test.ripple +93 -0
- package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
- package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
- package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
- package/tests/client/basic/basic.attributes.test.ripple +474 -0
- package/tests/client/basic/basic.collections.test.ripple +94 -0
- package/tests/client/basic/basic.components.test.ripple +225 -0
- package/tests/client/basic/basic.errors.test.ripple +126 -0
- package/tests/client/basic/basic.events.test.ripple +222 -0
- package/tests/client/basic/basic.reactivity.test.ripple +476 -0
- package/tests/client/basic/basic.rendering.test.ripple +204 -0
- package/tests/client/basic/basic.styling.test.ripple +63 -0
- package/tests/client/basic/basic.utilities.test.ripple +25 -0
- package/tests/client/boundaries.test.ripple +2 -21
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
- package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
- package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
- package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
- package/tests/client/compiler/compiler.basic.test.ripple +203 -0
- package/tests/client/compiler/compiler.regex.test.ripple +87 -0
- package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
- package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
- package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
- package/tests/client/composite/composite.generics.test.ripple +211 -0
- package/tests/client/composite/composite.props.test.ripple +106 -0
- package/tests/client/composite/composite.reactivity.test.ripple +184 -0
- package/tests/client/composite/composite.render.test.ripple +84 -0
- package/tests/client/computed-properties.test.ripple +2 -21
- package/tests/client/context.test.ripple +5 -22
- package/tests/client/date.test.ripple +1 -20
- package/tests/client/dynamic-elements.test.ripple +16 -24
- package/tests/client/for.test.ripple +4 -23
- package/tests/client/head.test.ripple +11 -23
- package/tests/client/html.test.ripple +1 -20
- package/tests/client/input-value.test.ripple +11 -31
- package/tests/client/map.test.ripple +82 -20
- package/tests/client/media-query.test.ripple +10 -23
- package/tests/client/object.test.ripple +5 -24
- package/tests/client/portal.test.ripple +2 -19
- package/tests/client/ref.test.ripple +8 -26
- package/tests/client/set.test.ripple +84 -22
- package/tests/client/svg.test.ripple +1 -22
- package/tests/client/switch.test.ripple +6 -25
- package/tests/client/tracked-expression.test.ripple +2 -21
- package/tests/client/typescript-generics.test.ripple +0 -21
- package/tests/client/url/url.derived.test.ripple +83 -0
- package/tests/client/url/url.parsing.test.ripple +165 -0
- package/tests/client/url/url.partial-removal.test.ripple +198 -0
- package/tests/client/url/url.reactivity.test.ripple +449 -0
- package/tests/client/url/url.serialization.test.ripple +50 -0
- package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
- package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
- package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
- package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
- package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
- package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
- package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
- package/tests/client.d.ts +12 -0
- package/tests/server/if.test.ripple +66 -0
- package/tests/setup-client.js +28 -0
- package/tsconfig.json +4 -2
- package/types/index.d.ts +92 -46
- package/LICENSE +0 -21
- package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
- package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
- package/tests/client/array.test.ripple +0 -1455
- package/tests/client/basic.test.ripple +0 -1892
- package/tests/client/compiler.test.ripple +0 -541
- package/tests/client/composite.test.ripple +0 -692
- package/tests/client/url-search-params.test.ripple +0 -912
- package/tests/client/url.test.ripple +0 -954
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { track, flushSync } from 'ripple';
|
|
2
|
+
|
|
3
|
+
describe('basic client > components & composition', () => {
|
|
4
|
+
it('renders with component composition and children', () => {
|
|
5
|
+
component Card(props) {
|
|
6
|
+
<div class='card'>
|
|
7
|
+
<props.children />
|
|
8
|
+
</div>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
component Basic() {
|
|
12
|
+
<Card>
|
|
13
|
+
component children() {
|
|
14
|
+
<p>{'Card content here'}</p>
|
|
15
|
+
}
|
|
16
|
+
</Card>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
render(Basic);
|
|
20
|
+
|
|
21
|
+
const card = container.querySelector('.card');
|
|
22
|
+
const paragraph = card.querySelector('p');
|
|
23
|
+
|
|
24
|
+
expect(card).toBeTruthy();
|
|
25
|
+
expect(paragraph.textContent).toBe('Card content here');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders with nested components and prop passing', () => {
|
|
29
|
+
component Button(props) {
|
|
30
|
+
<button class={props.variant} onClick={props.onClick}>
|
|
31
|
+
{props.label}
|
|
32
|
+
</button>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
component Card(props) {
|
|
36
|
+
<div class='card'>
|
|
37
|
+
<h3>{props.title}</h3>
|
|
38
|
+
<p>{props.content}</p>
|
|
39
|
+
<Button variant='primary' label={props.buttonText} onClick={props.onAction} />
|
|
40
|
+
</div>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
component Basic() {
|
|
44
|
+
let clicked = track(false);
|
|
45
|
+
|
|
46
|
+
<Card
|
|
47
|
+
title='Test Card'
|
|
48
|
+
content='This is a test card'
|
|
49
|
+
buttonText='Click me'
|
|
50
|
+
onAction={() => @clicked = true}
|
|
51
|
+
/>
|
|
52
|
+
<div class='status'>{@clicked ? 'Clicked' : 'Not clicked'}</div>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(Basic);
|
|
56
|
+
|
|
57
|
+
const card = container.querySelector('.card');
|
|
58
|
+
const title = card.querySelector('h3');
|
|
59
|
+
const content = card.querySelector('p');
|
|
60
|
+
const button = card.querySelector('button');
|
|
61
|
+
const status = container.querySelector('.status');
|
|
62
|
+
|
|
63
|
+
expect(title.textContent).toBe('Test Card');
|
|
64
|
+
expect(content.textContent).toBe('This is a test card');
|
|
65
|
+
expect(button.textContent).toBe('Click me');
|
|
66
|
+
expect(button.className).toBe('primary');
|
|
67
|
+
expect(status.textContent).toBe('Not clicked');
|
|
68
|
+
|
|
69
|
+
button.click();
|
|
70
|
+
flushSync();
|
|
71
|
+
|
|
72
|
+
expect(status.textContent).toBe('Clicked');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders with reactive component props', () => {
|
|
76
|
+
component ChildComponent(props) {
|
|
77
|
+
<div class='child-content'>{props.@text}</div>
|
|
78
|
+
<div class='child-count'>{props.@count}</div>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
component Basic() {
|
|
82
|
+
let message = track('Hello');
|
|
83
|
+
let number = track(1);
|
|
84
|
+
|
|
85
|
+
<ChildComponent text={message} count={number} />
|
|
86
|
+
<button onClick={() => {
|
|
87
|
+
@message = @message === 'Hello' ? 'Goodbye' : 'Hello';
|
|
88
|
+
@number++;
|
|
89
|
+
}}>{'Update Props'}</button>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
render(Basic);
|
|
93
|
+
|
|
94
|
+
const contentDiv = container.querySelector('.child-content');
|
|
95
|
+
const countDiv = container.querySelector('.child-count');
|
|
96
|
+
const button = container.querySelector('button');
|
|
97
|
+
|
|
98
|
+
expect(contentDiv.textContent).toBe('Hello');
|
|
99
|
+
expect(countDiv.textContent).toBe('1');
|
|
100
|
+
|
|
101
|
+
button.click();
|
|
102
|
+
flushSync();
|
|
103
|
+
|
|
104
|
+
expect(contentDiv.textContent).toBe('Goodbye');
|
|
105
|
+
expect(countDiv.textContent).toBe('2');
|
|
106
|
+
|
|
107
|
+
button.click();
|
|
108
|
+
flushSync();
|
|
109
|
+
|
|
110
|
+
expect(contentDiv.textContent).toBe('Hello');
|
|
111
|
+
expect(countDiv.textContent).toBe('3');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('it retains this context with bracketed prop functions and keeps original chaining', () => {
|
|
115
|
+
component App() {
|
|
116
|
+
const SYMBOL_PROP = Symbol();
|
|
117
|
+
let hasError = track(false);
|
|
118
|
+
const obj = {
|
|
119
|
+
count: track(0),
|
|
120
|
+
increment() {
|
|
121
|
+
this.@count++;
|
|
122
|
+
},
|
|
123
|
+
[SYMBOL_PROP]() {
|
|
124
|
+
this.@count++;
|
|
125
|
+
},
|
|
126
|
+
arr: [() => obj.@count++, () => obj.@count--],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const obj2 = null;
|
|
130
|
+
|
|
131
|
+
<button onClick={() => obj['increment']()}>{'Increment'}</button>
|
|
132
|
+
<button onClick={() => obj[SYMBOL_PROP]()}>{'Increment'}</button>
|
|
133
|
+
<button
|
|
134
|
+
onClick={() => {
|
|
135
|
+
@hasError = false;
|
|
136
|
+
try {
|
|
137
|
+
obj['nonexistent']();
|
|
138
|
+
} catch {
|
|
139
|
+
@hasError = true;
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
>{'Nonexistent'}</button>
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => {
|
|
145
|
+
@hasError = false;
|
|
146
|
+
try {
|
|
147
|
+
obj['nonexistent']?.();
|
|
148
|
+
} catch {
|
|
149
|
+
@hasError = true;
|
|
150
|
+
}
|
|
151
|
+
}}
|
|
152
|
+
>{'Nonexistent chaining'}</button>
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => {
|
|
155
|
+
@hasError = false;
|
|
156
|
+
try {
|
|
157
|
+
obj2['nonexistent']();
|
|
158
|
+
} catch {
|
|
159
|
+
@hasError = true;
|
|
160
|
+
}
|
|
161
|
+
}}
|
|
162
|
+
>{'Object null'}</button>
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => {
|
|
165
|
+
@hasError = false;
|
|
166
|
+
try {
|
|
167
|
+
obj2?.['nonexistent']?.();
|
|
168
|
+
} catch {
|
|
169
|
+
@hasError = true;
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
>{'Object null chained'}</button>
|
|
173
|
+
<button onClick={() => obj.arr[obj.arr.length - 1]()}>{'BinaryExpression prop'}</button>
|
|
174
|
+
|
|
175
|
+
<span>{obj.@count}</span>
|
|
176
|
+
<span>{@hasError}</span>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
render(App);
|
|
180
|
+
|
|
181
|
+
const button1 = container.querySelectorAll('button')[0];
|
|
182
|
+
const button2 = container.querySelectorAll('button')[1];
|
|
183
|
+
const button3 = container.querySelectorAll('button')[2];
|
|
184
|
+
const button4 = container.querySelectorAll('button')[3];
|
|
185
|
+
const button5 = container.querySelectorAll('button')[4];
|
|
186
|
+
const button6 = container.querySelectorAll('button')[5];
|
|
187
|
+
const button7 = container.querySelectorAll('button')[6];
|
|
188
|
+
|
|
189
|
+
const countSpan = container.querySelectorAll('span')[0];
|
|
190
|
+
const errorSpan = container.querySelectorAll('span')[1];
|
|
191
|
+
|
|
192
|
+
expect(countSpan.textContent).toBe('0');
|
|
193
|
+
expect(errorSpan.textContent).toBe('false');
|
|
194
|
+
|
|
195
|
+
button1.click();
|
|
196
|
+
flushSync();
|
|
197
|
+
|
|
198
|
+
expect(countSpan.textContent).toBe('1');
|
|
199
|
+
|
|
200
|
+
button2.click();
|
|
201
|
+
flushSync();
|
|
202
|
+
|
|
203
|
+
expect(countSpan.textContent).toBe('2');
|
|
204
|
+
|
|
205
|
+
button3.click();
|
|
206
|
+
flushSync();
|
|
207
|
+
expect(errorSpan.textContent).toBe('true');
|
|
208
|
+
|
|
209
|
+
button4.click();
|
|
210
|
+
flushSync();
|
|
211
|
+
expect(errorSpan.textContent).toBe('false');
|
|
212
|
+
|
|
213
|
+
button5.click();
|
|
214
|
+
flushSync();
|
|
215
|
+
expect(errorSpan.textContent).toBe('true');
|
|
216
|
+
|
|
217
|
+
button6.click();
|
|
218
|
+
flushSync();
|
|
219
|
+
expect(errorSpan.textContent).toBe('false');
|
|
220
|
+
|
|
221
|
+
button7.click();
|
|
222
|
+
flushSync();
|
|
223
|
+
expect(countSpan.textContent).toBe('1');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { track, flushSync, untrack } from 'ripple';
|
|
2
|
+
import { compile } from 'ripple/compiler';
|
|
3
|
+
|
|
4
|
+
describe('basic client > errors', () => {
|
|
5
|
+
it('renders with error handling simulation', () => {
|
|
6
|
+
component Basic() {
|
|
7
|
+
let hasError = track(false);
|
|
8
|
+
let errorMessage = track('');
|
|
9
|
+
|
|
10
|
+
const triggerError = () => {
|
|
11
|
+
try {
|
|
12
|
+
throw new Error('Test error');
|
|
13
|
+
} catch (e) {
|
|
14
|
+
@hasError = true;
|
|
15
|
+
@errorMessage = (e as Error).message;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
<div>
|
|
20
|
+
<button onClick={triggerError}>{'Trigger Error'}</button>
|
|
21
|
+
if (@hasError) {
|
|
22
|
+
<div class='error'>{'Error caught: ' + @errorMessage}</div>
|
|
23
|
+
} else {
|
|
24
|
+
<div class='success'>{'No error'}</div>
|
|
25
|
+
}
|
|
26
|
+
</div>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(Basic);
|
|
30
|
+
|
|
31
|
+
const button = container.querySelector('button');
|
|
32
|
+
const successDiv = container.querySelector('.success');
|
|
33
|
+
|
|
34
|
+
expect(successDiv).toBeTruthy();
|
|
35
|
+
expect(successDiv.textContent).toBe('No error');
|
|
36
|
+
|
|
37
|
+
button.click();
|
|
38
|
+
flushSync();
|
|
39
|
+
|
|
40
|
+
const errorDiv = container.querySelector('.error');
|
|
41
|
+
expect(errorDiv).toBeTruthy();
|
|
42
|
+
expect(errorDiv.textContent).toBe('Error caught: Test error');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should throw error for unclosed tag', () => {
|
|
46
|
+
const malformedCode = `export default component Example() {
|
|
47
|
+
<div></span>
|
|
48
|
+
}`;
|
|
49
|
+
expect(() => {
|
|
50
|
+
compile(malformedCode, 'test.ripple');
|
|
51
|
+
}).toThrow('Expected closing tag to match opening tag');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should throw error for completely unclosed tag', () => {
|
|
55
|
+
const malformedCode = `export default component Example() {
|
|
56
|
+
<div>content
|
|
57
|
+
}`;
|
|
58
|
+
|
|
59
|
+
expect(() => {
|
|
60
|
+
compile(malformedCode, 'test.ripple');
|
|
61
|
+
}).toThrow('Unclosed tag');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('errors on mutating tracked value inside computed track() evaluation', () => {
|
|
65
|
+
component Basic() {
|
|
66
|
+
let count = track(0);
|
|
67
|
+
|
|
68
|
+
const doubled = track(() => {
|
|
69
|
+
try {
|
|
70
|
+
@count *= 2;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
error = (e as Error).message;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
<p>{@doubled}</p>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render(Basic);
|
|
80
|
+
|
|
81
|
+
expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('errors on mutating tracked value inside untrack() in computed track() evaluation', () => {
|
|
85
|
+
component Basic() {
|
|
86
|
+
let count = track(0);
|
|
87
|
+
|
|
88
|
+
const doubled = track(() => {
|
|
89
|
+
try {
|
|
90
|
+
untrack(() => {
|
|
91
|
+
@count *= 2;
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {
|
|
94
|
+
error = (e as Error).message;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
<p>{@doubled}</p>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
render(Basic);
|
|
102
|
+
|
|
103
|
+
expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("errors on mutating a tracked variable in track() getter", () => {
|
|
107
|
+
component Basic() {
|
|
108
|
+
let count = track(0);
|
|
109
|
+
|
|
110
|
+
const doubled = track(0, (value) => {
|
|
111
|
+
try {
|
|
112
|
+
@count += 1;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
error = (e as Error).message;
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
<p>{@doubled}</p>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
render(Basic);
|
|
123
|
+
|
|
124
|
+
expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { track, flushSync } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('basic client > events', () => {
|
|
5
|
+
it('renders with different event types', () => {
|
|
6
|
+
component Basic() {
|
|
7
|
+
let focusCount = track(0);
|
|
8
|
+
let clickCount = track(0);
|
|
9
|
+
|
|
10
|
+
<button
|
|
11
|
+
onFocus={() => { @focusCount++ }}
|
|
12
|
+
onClick={() => { @clickCount++ }}
|
|
13
|
+
>{'Test Button'}</button>
|
|
14
|
+
<div class='focus-count'>{@focusCount}</div>
|
|
15
|
+
<div class='click-count'>{@clickCount}</div>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render(Basic);
|
|
19
|
+
|
|
20
|
+
const button = container.querySelector('button');
|
|
21
|
+
const focusDiv = container.querySelector('.focus-count');
|
|
22
|
+
const clickDiv = container.querySelector('.click-count');
|
|
23
|
+
|
|
24
|
+
button.dispatchEvent(new Event('focus'));
|
|
25
|
+
flushSync();
|
|
26
|
+
expect(focusDiv.textContent).toBe('1');
|
|
27
|
+
|
|
28
|
+
button.click();
|
|
29
|
+
flushSync();
|
|
30
|
+
expect(clickDiv.textContent).toBe('1');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders with capture events', () => {
|
|
34
|
+
component Basic() {
|
|
35
|
+
let captureClicks = track(0);
|
|
36
|
+
let bubbleClicks = track(0);
|
|
37
|
+
|
|
38
|
+
<div onClickCapture={() => { @captureClicks++ }}>
|
|
39
|
+
<button onClick={() => { @bubbleClicks++ }}>{'Click me'}</button>
|
|
40
|
+
<div class='capture-count'>{@captureClicks}</div>
|
|
41
|
+
<div class='bubble-count'>{@bubbleClicks}</div>
|
|
42
|
+
</div>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
render(Basic);
|
|
46
|
+
|
|
47
|
+
const button = container.querySelector('button');
|
|
48
|
+
const captureDiv = container.querySelector('.capture-count');
|
|
49
|
+
const bubbleDiv = container.querySelector('.bubble-count');
|
|
50
|
+
|
|
51
|
+
button.click();
|
|
52
|
+
flushSync();
|
|
53
|
+
|
|
54
|
+
expect(captureDiv.textContent).toBe('1');
|
|
55
|
+
expect(bubbleDiv.textContent).toBe('1');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders with event listeners in spread props', () => {
|
|
59
|
+
component Basic() {
|
|
60
|
+
let count = track(0);
|
|
61
|
+
|
|
62
|
+
const minus = {
|
|
63
|
+
onClick() {
|
|
64
|
+
@count--
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const plus = {
|
|
69
|
+
onClick() {
|
|
70
|
+
@count++
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
<div>
|
|
75
|
+
<button {...minus} class='minus'>{'-'}</button>
|
|
76
|
+
<span class='count'>{@count}</span>
|
|
77
|
+
<button {...plus} class='plus'>{'+'}</button>
|
|
78
|
+
</div>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
render(Basic);
|
|
82
|
+
|
|
83
|
+
const minusButton = container.querySelector('.minus');
|
|
84
|
+
const plusButton = container.querySelector('.plus');
|
|
85
|
+
const countSpan = container.querySelector('.count');
|
|
86
|
+
|
|
87
|
+
expect(countSpan.textContent).toBe('0');
|
|
88
|
+
|
|
89
|
+
// Test that the buttons don't have string onclick attributes
|
|
90
|
+
expect(minusButton.getAttribute('onclick')).toBe(null);
|
|
91
|
+
expect(plusButton.getAttribute('onclick')).toBe(null);
|
|
92
|
+
|
|
93
|
+
// Test that the event handlers work
|
|
94
|
+
minusButton.click();
|
|
95
|
+
flushSync();
|
|
96
|
+
expect(countSpan.textContent).toBe('-1');
|
|
97
|
+
|
|
98
|
+
plusButton.click();
|
|
99
|
+
flushSync();
|
|
100
|
+
expect(countSpan.textContent).toBe('0');
|
|
101
|
+
|
|
102
|
+
plusButton.click();
|
|
103
|
+
flushSync();
|
|
104
|
+
expect(countSpan.textContent).toBe('1');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('handles both delegated and non-delegated events in spread props', () => {
|
|
108
|
+
component Basic() {
|
|
109
|
+
let clickCount = track(0);
|
|
110
|
+
let focusCount = track(0);
|
|
111
|
+
|
|
112
|
+
const mixedHandler = {
|
|
113
|
+
onClick() { // Delegated event
|
|
114
|
+
@clickCount++
|
|
115
|
+
},
|
|
116
|
+
onFocus() { // Non-delegated event
|
|
117
|
+
@focusCount++
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
<div>
|
|
122
|
+
<button {...mixedHandler} class='mixed-button'>{'Test'}</button>
|
|
123
|
+
<span class='click-count'>{@clickCount}</span>
|
|
124
|
+
<span class='focus-count'>{@focusCount}</span>
|
|
125
|
+
</div>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
render(Basic);
|
|
129
|
+
|
|
130
|
+
const button = container.querySelector('.mixed-button');
|
|
131
|
+
const clickSpan = container.querySelector('.click-count');
|
|
132
|
+
const focusSpan = container.querySelector('.focus-count');
|
|
133
|
+
|
|
134
|
+
expect(clickSpan.textContent).toBe('0');
|
|
135
|
+
expect(focusSpan.textContent).toBe('0');
|
|
136
|
+
|
|
137
|
+
// Test delegated event (click)
|
|
138
|
+
button.click();
|
|
139
|
+
flushSync();
|
|
140
|
+
expect(clickSpan.textContent).toBe('1');
|
|
141
|
+
|
|
142
|
+
// Test non-delegated event (focus)
|
|
143
|
+
button.dispatchEvent(new Event('focus'));
|
|
144
|
+
flushSync();
|
|
145
|
+
expect(focusSpan.textContent).toBe('1');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('renders with complex event handling and state updates', () => {
|
|
149
|
+
component Basic() {
|
|
150
|
+
let counter = track(0);
|
|
151
|
+
let history = track<string[]>([]);
|
|
152
|
+
let isEven = track(true);
|
|
153
|
+
|
|
154
|
+
const handleIncrement = () => {
|
|
155
|
+
@counter++;
|
|
156
|
+
@history = [...@history, `Inc to ${@counter}`];
|
|
157
|
+
@isEven = @counter % 2 === 0;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleDecrement = () => {
|
|
161
|
+
@counter--;
|
|
162
|
+
@history = [...@history, `Dec to ${@counter}`];
|
|
163
|
+
@isEven = @counter % 2 === 0;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleReset = () => {
|
|
167
|
+
@counter = 0;
|
|
168
|
+
@history = [...@history, 'Reset'];
|
|
169
|
+
@isEven = true;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
<div class='counter'>{@counter}</div>
|
|
173
|
+
<div class='parity'>{@isEven ? 'Even' : 'Odd'}</div>
|
|
174
|
+
<div class='history-count'>{@history.length}</div>
|
|
175
|
+
|
|
176
|
+
<button class='inc-btn' onClick={handleIncrement}>{'+'}</button>
|
|
177
|
+
<button class='dec-btn' onClick={handleDecrement}>{'-'}</button>
|
|
178
|
+
<button class='reset-btn' onClick={handleReset}>{'Reset'}</button>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
render(Basic);
|
|
182
|
+
|
|
183
|
+
const counterDiv = container.querySelector('.counter');
|
|
184
|
+
const parityDiv = container.querySelector('.parity');
|
|
185
|
+
const historyDiv = container.querySelector('.history-count');
|
|
186
|
+
const incBtn = container.querySelector('.inc-btn');
|
|
187
|
+
const decBtn = container.querySelector('.dec-btn');
|
|
188
|
+
const resetBtn = container.querySelector('.reset-btn');
|
|
189
|
+
|
|
190
|
+
expect(counterDiv.textContent).toBe('0');
|
|
191
|
+
expect(parityDiv.textContent).toBe('Even');
|
|
192
|
+
expect(historyDiv.textContent).toBe('0');
|
|
193
|
+
|
|
194
|
+
incBtn.click();
|
|
195
|
+
flushSync();
|
|
196
|
+
|
|
197
|
+
expect(counterDiv.textContent).toBe('1');
|
|
198
|
+
expect(parityDiv.textContent).toBe('Odd');
|
|
199
|
+
expect(historyDiv.textContent).toBe('1');
|
|
200
|
+
|
|
201
|
+
incBtn.click();
|
|
202
|
+
flushSync();
|
|
203
|
+
|
|
204
|
+
expect(counterDiv.textContent).toBe('2');
|
|
205
|
+
expect(parityDiv.textContent).toBe('Even');
|
|
206
|
+
expect(historyDiv.textContent).toBe('2');
|
|
207
|
+
|
|
208
|
+
decBtn.click();
|
|
209
|
+
flushSync();
|
|
210
|
+
|
|
211
|
+
expect(counterDiv.textContent).toBe('1');
|
|
212
|
+
expect(parityDiv.textContent).toBe('Odd');
|
|
213
|
+
expect(historyDiv.textContent).toBe('3');
|
|
214
|
+
|
|
215
|
+
resetBtn.click();
|
|
216
|
+
flushSync();
|
|
217
|
+
|
|
218
|
+
expect(counterDiv.textContent).toBe('0');
|
|
219
|
+
expect(parityDiv.textContent).toBe('Even');
|
|
220
|
+
expect(historyDiv.textContent).toBe('4');
|
|
221
|
+
});
|
|
222
|
+
});
|