ripple 0.2.68 → 0.2.70

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.
@@ -378,6 +378,18 @@ export function is_component_level_function(context) {
378
378
  return true;
379
379
  }
380
380
 
381
+ export function is_ripple_track_call(callee, context) {
382
+ return (
383
+ (callee.type === 'Identifier' && callee.name === 'track') ||
384
+ (callee.type === 'MemberExpression' &&
385
+ callee.object.type === 'Identifier' &&
386
+ callee.property.type === 'Identifier' &&
387
+ callee.property.name === 'track' &&
388
+ !callee.computed &&
389
+ is_ripple_import(callee, context))
390
+ );
391
+ }
392
+
381
393
  export function is_inside_call_expression(context) {
382
394
  for (let i = context.path.length - 1; i >= 0; i -= 1) {
383
395
  const context_node = context.path[i];
@@ -391,6 +403,10 @@ export function is_inside_call_expression(context) {
391
403
  return false;
392
404
  }
393
405
  if (type === 'CallExpression') {
406
+ const callee = context_node.callee;
407
+ if (is_ripple_track_call(callee, context)) {
408
+ return false;
409
+ }
394
410
  return true;
395
411
  }
396
412
  }
package/src/constants.js CHANGED
@@ -1,3 +1,7 @@
1
1
  export const TEMPLATE_FRAGMENT = 1;
2
2
  export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
3
3
  export const IS_CONTROLLED = 1 << 2;
4
+ export const IS_INDEXED = 1 << 3;
5
+ export const TEMPLATE_SVG_NAMESPACE = 1 << 3;
6
+ export const TEMPLATE_MATHML_NAMESPACE = 1 << 4;
7
+
@@ -105,17 +105,24 @@ function proxy({ elements, block, from_static = false, use_array = false }) {
105
105
 
106
106
  var result = Reflect.get(target, prop, receiver);
107
107
 
108
- if (typeof result === "function" && methods_returning_arrays.has(prop)) {
109
- /** @type {(this: any, ...args: any[]) => any} */
110
- return function (...args) {
111
- var output = Reflect.apply(result, receiver, args)
112
-
113
- if (Array.isArray(output) && output !== target) {
114
- return proxy({ elements: output, block, use_array: true });
115
- }
108
+ if (typeof result === 'function') {
109
+ if (methods_returning_arrays.has(prop)) {
110
+ /** @type {(this: any, ...args: any[]) => any} */
111
+ return function (...args) {
112
+ var output = Reflect.apply(result, receiver, args);
113
+
114
+ if (Array.isArray(output) && output !== target) {
115
+ return proxy({ elements: output, block, use_array: true });
116
+ }
117
+
118
+ return output;
119
+ };
120
+ }
116
121
 
117
- return output;
118
- };
122
+ // When generating an iterator, we need to ensure that length is tracked
123
+ if (prop === 'entries' || prop === 'values' || prop === 'keys') {
124
+ receiver.length;
125
+ }
119
126
  }
120
127
 
121
128
  return result;
@@ -229,14 +236,16 @@ function proxy({ elements, block, from_static = false, use_array = false }) {
229
236
  // target object — which we avoid, so that state can be forked — we will run
230
237
  // afoul of the various invariants
231
238
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants
232
- throw new Error('Only basic property descriptors are supported with value and configurable, enumerable, and writable set to true');
239
+ throw new Error(
240
+ 'Only basic property descriptors are supported with value and configurable, enumerable, and writable set to true',
241
+ );
233
242
  }
234
243
 
235
244
  var t = tracked_elements.get(prop);
236
245
 
237
246
  if (t === undefined) {
238
- t = tracked(descriptor.value, block);
239
- tracked_elements.set(prop, t);
247
+ t = tracked(descriptor.value, block);
248
+ tracked_elements.set(prop, t);
240
249
  } else {
241
250
  set(t, descriptor.value, block);
242
251
  }
@@ -266,15 +275,15 @@ function get_first_if_length(array) {
266
275
  }
267
276
 
268
277
  const methods_returning_arrays = new Set([
269
- "concat",
270
- "filter",
271
- "flat",
272
- "flatMap",
273
- "map",
274
- "slice",
275
- "splice",
276
- "toReversed",
277
- "toSorted",
278
- "toSpliced",
279
- "with",
278
+ 'concat',
279
+ 'filter',
280
+ 'flat',
281
+ 'flatMap',
282
+ 'map',
283
+ 'slice',
284
+ 'splice',
285
+ 'toReversed',
286
+ 'toSorted',
287
+ 'toSpliced',
288
+ 'with',
280
289
  ]);
@@ -1,13 +1,32 @@
1
- import { IS_CONTROLLED } from '../../../constants.js';
1
+ import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
2
2
  import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
3
3
  import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
4
- import { create_text } from './operations.js';
5
- import { active_block, untrack } from './runtime.js';
4
+ import { create_text, next_sibling } from './operations.js';
5
+ import { active_block, set, tracked, untrack } from './runtime.js';
6
6
  import { array_from, is_array } from './utils.js';
7
7
 
8
- function create_item(anchor, value, render_fn) {
8
+ function create_item(anchor, value, index, render_fn, is_indexed) {
9
9
  var b = branch(() => {
10
- render_fn(anchor, value);
10
+ var tracked_index;
11
+
12
+ if (is_indexed) {
13
+ var block = /** @type {Block} */ (active_block);
14
+
15
+ if (block.s === null) {
16
+ tracked_index = tracked(index, block);
17
+
18
+ block.s = {
19
+ start: null,
20
+ end: null,
21
+ i: tracked_index,
22
+ };
23
+ } else {
24
+ tracked_index = block.s.i;
25
+ }
26
+ render_fn(anchor, value, tracked_index);
27
+ } else {
28
+ render_fn(anchor, value);
29
+ }
11
30
  });
12
31
  return b;
13
32
  }
@@ -21,14 +40,27 @@ function move(block, anchor) {
21
40
  return;
22
41
  }
23
42
  while (node !== end) {
24
- var next_node = /** @type {TemplateNode} */ (get_next_sibling(node));
43
+ var next_node = /** @type {TemplateNode} */ (next_sibling(node));
25
44
  anchor.before(node);
26
45
  node = next_node;
27
46
  }
28
47
  }
29
48
 
49
+ function collection_to_array(collection) {
50
+ var array = is_array(collection) ? collection : collection == null ? [] : array_from(collection);
51
+
52
+ // If we are working with a tracked array, then we need to get a copy of
53
+ // the elements, as the array itself is proxied, and not useful in diffing
54
+ if (TRACKED_ARRAY in array) {
55
+ array = array_from(array);
56
+ }
57
+
58
+ return array;
59
+ }
60
+
30
61
  export function for_block(node, get_collection, render_fn, flags) {
31
62
  var is_controlled = (flags & IS_CONTROLLED) !== 0;
63
+ var is_indexed = (flags & IS_INDEXED) !== 0;
32
64
  var anchor = node;
33
65
 
34
66
  if (is_controlled) {
@@ -36,21 +68,13 @@ export function for_block(node, get_collection, render_fn, flags) {
36
68
  }
37
69
 
38
70
  render(() => {
39
- var block = active_block;
71
+ var block = /** @type {Block} */ (active_block);
40
72
  var collection = get_collection();
41
- var array = is_array(collection)
42
- ? collection
43
- : collection == null
44
- ? []
45
- : array_from(collection);
46
-
47
- // If we are working with a tracked array, then we need to get a copy of
48
- // the elements, as the array itself is proxied, and not useful in diffing
49
- if (TRACKED_ARRAY in array) {
50
- array = array_from(array);
51
- }
73
+ var array = collection_to_array(collection);
52
74
 
53
- untrack(() => reconcile(anchor, block, array, render_fn, is_controlled));
75
+ untrack(() => {
76
+ reconcile(anchor, block, array, render_fn, is_controlled, is_indexed);
77
+ });
54
78
  }, FOR_BLOCK);
55
79
  }
56
80
 
@@ -64,7 +88,11 @@ function reconcile_fast_clear(anchor, block, array) {
64
88
  state.blocks = [];
65
89
  }
66
90
 
67
- function reconcile(anchor, block, b, render_fn, is_controlled) {
91
+ function update_index(block, index) {
92
+ set(block.s.i, index, block);
93
+ }
94
+
95
+ function reconcile(anchor, block, b, render_fn, is_controlled, is_indexed) {
68
96
  var state = block.s;
69
97
 
70
98
  if (state === null) {
@@ -91,7 +119,7 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
91
119
  // Fast-path for create
92
120
  if (a_length === 0) {
93
121
  for (; j < b_length; j++) {
94
- b_blocks[j] = create_item(anchor, b[j], render_fn);
122
+ b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed);
95
123
  }
96
124
  state.array = b;
97
125
  state.blocks = b_blocks;
@@ -103,11 +131,15 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
103
131
  var b_val = b[j];
104
132
  var a_end = a_length - 1;
105
133
  var b_end = b_length - 1;
134
+ var b_block;
106
135
 
107
136
  outer: {
108
137
  while (a_val === b_val) {
109
138
  a[j] = b_val;
110
- b_blocks[j] = a_blocks[j];
139
+ b_block = b_blocks[j] = a_blocks[j];
140
+ if (is_indexed) {
141
+ update_index(b_block, j);
142
+ }
111
143
  ++j;
112
144
  if (j > a_end || j > b_end) {
113
145
  break outer;
@@ -121,7 +153,10 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
121
153
 
122
154
  while (a_val === b_val) {
123
155
  a[a_end] = b_val;
124
- b_blocks[b_end] = a_blocks[a_end];
156
+ b_block = b_blocks[b_end] = a_blocks[a_end];
157
+ if (is_indexed) {
158
+ update_index(b_block, b_end);
159
+ }
125
160
  a_end--;
126
161
  b_end--;
127
162
  if (j > a_end || j > b_end) {
@@ -137,7 +172,8 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
137
172
  while (j <= b_end) {
138
173
  b_val = b[j];
139
174
  var target = j >= a_length ? anchor : a_blocks[j].s.start;
140
- b_blocks[j++] = create_item(target, b_val, render_fn);
175
+ b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed);
176
+ j++;
141
177
  }
142
178
  }
143
179
  } else if (j > b_end) {
@@ -177,7 +213,10 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
177
213
  } else {
178
214
  pos = j;
179
215
  }
180
- b_blocks[j] = a_blocks[i];
216
+ b_block = b_blocks[j] = a_blocks[i];
217
+ if (is_indexed) {
218
+ update_index(b_block, j);
219
+ }
181
220
  ++patched;
182
221
  break;
183
222
  }
@@ -216,7 +255,10 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
216
255
  pos = j;
217
256
  }
218
257
  b_val = b[j];
219
- b_blocks[j] = a_blocks[i];
258
+ block = b_blocks[j] = a_blocks[i];
259
+ if (is_indexed) {
260
+ update_index(block, j);
261
+ }
220
262
  ++patched;
221
263
  } else if (!fast_path_removal) {
222
264
  destroy_block(a_blocks[i]);
@@ -244,7 +286,7 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
244
286
  next_pos = pos + 1;
245
287
 
246
288
  var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
247
- b_blocks[pos] = create_item(target, b_val, render_fn);
289
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
248
290
  } else if (j < 0 || i !== seq[j]) {
249
291
  pos = i + b_start;
250
292
  b_val = b[pos];
@@ -264,7 +306,7 @@ function reconcile(anchor, block, b, render_fn, is_controlled) {
264
306
  next_pos = pos + 1;
265
307
 
266
308
  var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
267
- b_blocks[pos] = create_item(target, b_val, render_fn);
309
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
268
310
  }
269
311
  }
270
312
  }
@@ -361,11 +403,7 @@ export function keyed(collection, key_fn) {
361
403
  throw new Error('Duplicate keys are not allowed');
362
404
  }
363
405
 
364
- var b_array = is_array(collection)
365
- ? collection
366
- : collection == null
367
- ? []
368
- : array_from(collection);
406
+ var b_array = collection_to_array(collection);
369
407
  var b_keys = b_array.map(key_fn);
370
408
 
371
409
  // We only need to do this in DEV
@@ -15,7 +15,7 @@ import {
15
15
  import { get } from './runtime.js';
16
16
 
17
17
  export function set_text(text, value) {
18
- // For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
18
+ // For objects, we apply string coercion
19
19
  var str = value == null ? '' : typeof value === 'object' ? value + '' : value;
20
20
  // @ts-expect-error
21
21
  if (str !== (text.__t ??= text.nodeValue)) {
@@ -84,7 +84,8 @@ export function set_attributes(element, attributes) {
84
84
  }
85
85
 
86
86
  if (key === 'class') {
87
- set_class(element, value);
87
+ const is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
88
+ set_class(element, value, undefined, is_html);
88
89
  } else if (key === '#class') {
89
90
  // Special case for static class when spreading props
90
91
  element.classList.add(value);
@@ -120,9 +121,10 @@ function to_class(value, hash) {
120
121
  * @param {HTMLElement} dom
121
122
  * @param {string} value
122
123
  * @param {string} [hash]
124
+ * @param {boolean} [is_html]
123
125
  * @returns {void}
124
126
  */
125
- export function set_class(dom, value, hash) {
127
+ export function set_class(dom, value, hash, is_html = true) {
126
128
  // @ts-expect-error need to add __className to patched prototype
127
129
  var prev_class_name = dom.__className;
128
130
  var next_class_name = to_class(value, hash);
@@ -134,7 +136,11 @@ export function set_class(dom, value, hash) {
134
136
  if (value == null && !hash) {
135
137
  dom.removeAttribute('class');
136
138
  } else {
137
- dom.className = next_class_name;
139
+ if (is_html) {
140
+ dom.className = next_class_name;
141
+ } else {
142
+ dom.setAttribute('class', next_class_name);
143
+ }
138
144
  }
139
145
 
140
146
  // @ts-expect-error need to add __className to patched prototype
@@ -667,6 +667,11 @@ export function get_derived(computed) {
667
667
  * @param {Derived | Tracked} tracked
668
668
  */
669
669
  export function get(tracked) {
670
+ // reflect back the value if it's not boxed
671
+ if (!is_tracked_object(tracked)) {
672
+ return tracked;
673
+ }
674
+
670
675
  return (tracked.f & DERIVED) !== 0
671
676
  ? get_derived(/** @type {Derived} */ (tracked))
672
677
  : get_tracked(tracked);
@@ -1,4 +1,9 @@
1
- import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
1
+ import {
2
+ TEMPLATE_FRAGMENT,
3
+ TEMPLATE_USE_IMPORT_NODE,
4
+ TEMPLATE_SVG_NAMESPACE,
5
+ TEMPLATE_MATHML_NAMESPACE,
6
+ } from '../../../constants.js';
2
7
  import { first_child, is_firefox } from './operations.js';
3
8
  import { active_block } from './runtime.js';
4
9
 
@@ -8,21 +13,33 @@ import { active_block } from './runtime.js';
8
13
  * @param {Node} end - The end node.
9
14
  */
10
15
  export function assign_nodes(start, end) {
11
- var block = /** @type {Effect} */ (active_block);
12
- if (block.s === null) {
16
+ var block = /** @type {Block} */ (active_block);
17
+ var s = block.s;
18
+ if (s === null) {
13
19
  block.s = {
14
20
  start,
15
21
  end,
16
22
  };
23
+ } else if (s.start === null) {
24
+ s.start = start;
25
+ s.end = end;
17
26
  }
18
27
  }
19
28
 
20
29
  /**
21
30
  * Creates a DocumentFragment from an HTML string.
22
31
  * @param {string} html - The HTML string.
32
+ * @param {boolean} use_svg_namespace - Whether to use SVG namespace.
33
+ * @param {boolean} use_mathml_namespace - Whether to use MathML namespace.
23
34
  * @returns {DocumentFragment}
24
35
  */
25
- function create_fragment_from_html(html) {
36
+ function create_fragment_from_html(html, use_svg_namespace = false, use_mathml_namespace = false) {
37
+ if (use_svg_namespace) {
38
+ return from_namespace(html, 'svg');
39
+ }
40
+ if (use_mathml_namespace) {
41
+ return from_namespace(html, 'math');
42
+ }
26
43
  var elem = document.createElement('template');
27
44
  elem.innerHTML = html;
28
45
  return elem.content;
@@ -37,12 +54,18 @@ function create_fragment_from_html(html) {
37
54
  export function template(content, flags) {
38
55
  var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
39
56
  var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
57
+ var use_svg_namespace = (flags & TEMPLATE_SVG_NAMESPACE) !== 0;
58
+ var use_mathml_namespace = (flags & TEMPLATE_MATHML_NAMESPACE) !== 0;
40
59
  var node;
41
60
  var has_start = !content.startsWith('<!>');
42
61
 
43
62
  return () => {
44
63
  if (node === undefined) {
45
- node = create_fragment_from_html(has_start ? content : '<!>' + content);
64
+ node = create_fragment_from_html(
65
+ has_start ? content : '<!>' + content,
66
+ use_svg_namespace,
67
+ use_mathml_namespace,
68
+ );
46
69
  if (!is_fragment) node = first_child(node);
47
70
  }
48
71
 
@@ -70,3 +93,26 @@ export function template(content, flags) {
70
93
  export function append(anchor, dom) {
71
94
  anchor.before(/** @type {Node} */ (dom));
72
95
  }
96
+
97
+ /**
98
+ * Create fragment with proper namespace using Svelte's wrapping approach
99
+ * @param {string} content
100
+ * @param {'svg' | 'math'} ns
101
+ * @returns {DocumentFragment}
102
+ */
103
+ function from_namespace(content, ns = 'svg') {
104
+ var wrapped = `<${ns}>${content}</${ns}>`;
105
+
106
+ var elem = document.createElement('template');
107
+ elem.innerHTML = wrapped;
108
+ var fragment = elem.content;
109
+
110
+ var root = /** @type {Element} */ (first_child(fragment));
111
+ var result = document.createDocumentFragment();
112
+
113
+ while (first_child(root)) {
114
+ result.appendChild(/** @type {Node} */ (first_child(root)));
115
+ }
116
+
117
+ return result;
118
+ }
@@ -18,6 +18,24 @@ exports[`basic > basic operations 1`] = `
18
18
  </div>
19
19
  `;
20
20
 
21
+ exports[`basic > correctly renders adjacent text nodes 1`] = `
22
+ <div>
23
+ <div>
24
+ 11
25
+ </div>
26
+ <div>
27
+ 11
28
+ </div>
29
+ <div>
30
+ truetrue
31
+ </div>
32
+ <div>
33
+ truetrue
34
+ </div>
35
+
36
+ </div>
37
+ `;
38
+
21
39
  exports[`basic > handles boolean attributes with no prop value provides 1`] = `
22
40
  <div>
23
41
  <div
@@ -21,3 +21,17 @@ exports[`composite components > correct handles passing through component props
21
21
 
22
22
  </div>
23
23
  `;
24
+
25
+ exports[`composite components > render simple text as children 1`] = `
26
+ <div>
27
+ <!---->
28
+ <button
29
+ class="my-button"
30
+ >
31
+ Click Me
32
+ <!---->
33
+ </button>
34
+ <!---->
35
+
36
+ </div>
37
+ `;
@@ -1,5 +1,83 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
+ exports[`for statements > correctly handle the index in a for...of loop 1`] = `
4
+ <div>
5
+ <div>
6
+ <div>
7
+ 0 : a
8
+ </div>
9
+ <div>
10
+ 1 : b
11
+ </div>
12
+ <div>
13
+ 2 : c
14
+ </div>
15
+
16
+ </div>
17
+ <button>
18
+ Add Item
19
+ </button>
20
+ <button>
21
+ Reverse
22
+ </button>
23
+
24
+ </div>
25
+ `;
26
+
27
+ exports[`for statements > correctly handle the index in a for...of loop 2`] = `
28
+ <div>
29
+ <div>
30
+ <div>
31
+ 0 : a
32
+ </div>
33
+ <div>
34
+ 1 : b
35
+ </div>
36
+ <div>
37
+ 2 : c
38
+ </div>
39
+ <div>
40
+ 3 : d
41
+ </div>
42
+
43
+ </div>
44
+ <button>
45
+ Add Item
46
+ </button>
47
+ <button>
48
+ Reverse
49
+ </button>
50
+
51
+ </div>
52
+ `;
53
+
54
+ exports[`for statements > correctly handle the index in a for...of loop 3`] = `
55
+ <div>
56
+ <div>
57
+ <div>
58
+ 0 : d
59
+ </div>
60
+ <div>
61
+ 1 : c
62
+ </div>
63
+ <div>
64
+ 2 : b
65
+ </div>
66
+ <div>
67
+ 3 : a
68
+ </div>
69
+
70
+ </div>
71
+ <button>
72
+ Add Item
73
+ </button>
74
+ <button>
75
+ Reverse
76
+ </button>
77
+
78
+ </div>
79
+ `;
80
+
3
81
  exports[`for statements > correctly handles intermediate statements in for block 1`] = `
4
82
  <div>
5
83
  <div>
@@ -1271,4 +1271,19 @@ describe('basic', () => {
1271
1271
  render(App);
1272
1272
  expect(container).toMatchSnapshot();
1273
1273
  });
1274
+
1275
+ it('correctly renders adjacent text nodes', () => {
1276
+ component App() {
1277
+ let a = 1;
1278
+ let b = 1;
1279
+ <div>{1}{1}</div>
1280
+ <div>{a}{b}</div>
1281
+ <div>{true}{true}</div>
1282
+ <div>{true}{true}</div>
1283
+ }
1284
+
1285
+ render(App);
1286
+
1287
+ expect(container).toMatchSnapshot();
1288
+ });
1274
1289
  });
@@ -647,4 +647,21 @@ describe('composite components', () => {
647
647
 
648
648
  expect(container.textContent).toBe('Basic Component');
649
649
  });
650
+
651
+ it('render simple text as children', () => {
652
+ component App() {
653
+ let name = 'Click Me';
654
+
655
+ <Child
656
+ class="my-button"
657
+ >{name}</Child>;
658
+ }
659
+
660
+ component Child({children, ...rest}) {
661
+ <button {...rest}><children /></button>
662
+ }
663
+
664
+ render(App);
665
+ expect(container).toMatchSnapshot();
666
+ })
650
667
  });
@@ -1,5 +1,4 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
-
3
2
  import { mount, flushSync, TrackedArray } from 'ripple';
4
3
 
5
4
  describe('for statements', () => {
@@ -84,5 +83,36 @@ describe('for statements', () => {
84
83
  flushSync();
85
84
 
86
85
  expect(container).toMatchSnapshot();
87
- })
86
+ });
87
+
88
+ it('correctly handle the index in a for...of loop', () => {
89
+ component App() {
90
+ const items = new TrackedArray('a', 'b', 'c');
91
+
92
+ <div>
93
+ for (let item of items; index i) {
94
+ <div>{i + ' : ' + item}</div>
95
+ }
96
+ </div>
97
+
98
+ <button onClick={() => items.push(String.fromCharCode(97 + items.length))}>{'Add Item'}</button>
99
+ <button onClick={() => items.reverse()}>{'Reverse'}</button>
100
+ }
101
+
102
+ render(App);
103
+
104
+ expect(container).toMatchSnapshot();
105
+
106
+ const [button, button2] = container.querySelectorAll('button');
107
+
108
+ button.click();
109
+ flushSync();
110
+
111
+ expect(container).toMatchSnapshot();
112
+
113
+ button2.click();
114
+ flushSync();
115
+
116
+ expect(container).toMatchSnapshot();
117
+ });
88
118
  });