ripple 0.3.85 → 0.3.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/package.json +3 -3
- package/src/runtime/internal/client/blocks.js +53 -0
- package/src/runtime/internal/client/for.js +65 -13
- package/src/runtime/internal/client/switch.js +17 -3
- package/tests/client/basic/basic.rendering.test.tsrx +22 -0
- package/tests/client/for-single-root-items.test.tsrx +130 -0
- package/tests/hydration/compiled/client/if-fragment-controlflow.js +463 -0
- package/tests/hydration/compiled/server/basic.js +27 -0
- package/tests/hydration/compiled/server/if-fragment-controlflow.js +227 -0
- package/tests/hydration/components/if-fragment-controlflow.tsrx +145 -0
- package/tests/hydration/if-fragment-controlflow.test.js +95 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.87
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
[[`cc95ffa`](https://github.com/Ripple-TS/ripple/commit/cc95ffaef3f3d3cd252176ea94308f89739f0212),
|
|
9
|
+
[`6f78b7f`](https://github.com/Ripple-TS/ripple/commit/6f78b7ff5a5e1f9873a839b709f38e9506545a63)]:
|
|
10
|
+
- @tsrx/core@0.1.34
|
|
11
|
+
- @tsrx/ripple@0.1.35
|
|
12
|
+
|
|
13
|
+
## 0.3.86
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#1310](https://github.com/Ripple-TS/ripple/pull/1310)
|
|
18
|
+
[`4ebd58d`](https://github.com/Ripple-TS/ripple/commit/4ebd58dfe853c1ed945072822eaba8a7a9e19a6c)
|
|
19
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Fix
|
|
20
|
+
`Cannot use 'in' operator to search for 'parent' in null` thrown from `append()`
|
|
21
|
+
when reordering or inserting into a keyed `@for` whose item body is a single
|
|
22
|
+
control-flow / component root (e.g. `@for (...; key ...) { <Card {item} /> }`
|
|
23
|
+
where `Card`'s own body is a single `@if`/`@for`/component). The 0.3.85
|
|
24
|
+
wrapper-anchor optimization leaves such an item block's `s.start` null because
|
|
25
|
+
its DOM is rendered through a descendant block, and keyed reconciliation read
|
|
26
|
+
`s.start` directly as the insertion anchor. Reconciliation now resolves the real
|
|
27
|
+
first/last DOM node by descending child blocks, so no `<!>` comment marker is
|
|
28
|
+
reintroduced and the optimization's reduced DOM-mutation cost is preserved. The
|
|
29
|
+
same resolution is applied to `@switch` case reordering.
|
|
30
|
+
- Updated dependencies
|
|
31
|
+
[[`e4e6d7b`](https://github.com/Ripple-TS/ripple/commit/e4e6d7b854786ad19a2c86276ea7e0ffb062e61a)]:
|
|
32
|
+
- @tsrx/ripple@0.1.34
|
|
33
|
+
|
|
3
34
|
## 0.3.85
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is an elegant TypeScript UI framework",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.87",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"clsx": "^2.1.1",
|
|
74
74
|
"devalue": "^5.8.1",
|
|
75
75
|
"esm-env": "^1.2.2",
|
|
76
|
-
"@tsrx/core": "0.1.
|
|
77
|
-
"@tsrx/ripple": "0.1.
|
|
76
|
+
"@tsrx/core": "0.1.34",
|
|
77
|
+
"@tsrx/ripple": "0.1.35"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@types/estree": "^1.0.8",
|
|
@@ -429,6 +429,59 @@ export function move_block(block, target) {
|
|
|
429
429
|
return moved;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Resolve the first DOM node owned by a block. A block normally records its
|
|
434
|
+
* range in `s.start`/`s.end`, but an optimized single control-flow / component
|
|
435
|
+
* root scope renders its content through a descendant block instead of a
|
|
436
|
+
* synthesized `<!>` wrapper, so its own `s.start` is null. In that case we
|
|
437
|
+
* descend into child blocks to find the real first node. Returns null when the
|
|
438
|
+
* block currently renders no DOM.
|
|
439
|
+
* @param {Block} block
|
|
440
|
+
* @returns {Node | null}
|
|
441
|
+
*/
|
|
442
|
+
export function get_first_node(block) {
|
|
443
|
+
var f = block.f;
|
|
444
|
+
if ((f & BRANCH_BLOCK) !== 0 && (f & TRY_BLOCK) === 0) {
|
|
445
|
+
var s = block.s;
|
|
446
|
+
if (s !== null && s.start !== null) {
|
|
447
|
+
return s.start;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
var child = block.first;
|
|
451
|
+
while (child !== null) {
|
|
452
|
+
var node = get_first_node(child);
|
|
453
|
+
if (node !== null) {
|
|
454
|
+
return node;
|
|
455
|
+
}
|
|
456
|
+
child = child.next;
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Mirror of {@link get_first_node} for the last DOM node owned by a block.
|
|
463
|
+
* @param {Block} block
|
|
464
|
+
* @returns {Node | null}
|
|
465
|
+
*/
|
|
466
|
+
export function get_last_node(block) {
|
|
467
|
+
var f = block.f;
|
|
468
|
+
if ((f & BRANCH_BLOCK) !== 0 && (f & TRY_BLOCK) === 0) {
|
|
469
|
+
var s = block.s;
|
|
470
|
+
if (s !== null && s.start !== null) {
|
|
471
|
+
return s.end;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
var child = block.last;
|
|
475
|
+
while (child !== null) {
|
|
476
|
+
var node = get_last_node(child);
|
|
477
|
+
if (node !== null) {
|
|
478
|
+
return node;
|
|
479
|
+
}
|
|
480
|
+
child = child.prev;
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
432
485
|
/**
|
|
433
486
|
* @param {Block} block
|
|
434
487
|
* @param {boolean} [remove_dom]
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/** @import { Block, Tracked } from '#client' */
|
|
2
2
|
|
|
3
3
|
import { IS_CONTROLLED, IS_INDEXED, ROOT_CONTROLLED } from '../../../constants.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
branch,
|
|
6
|
+
destroy_block,
|
|
7
|
+
destroy_block_children,
|
|
8
|
+
get_first_node,
|
|
9
|
+
get_last_node,
|
|
10
|
+
render,
|
|
11
|
+
} from './blocks.js';
|
|
5
12
|
import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
|
|
6
13
|
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
|
|
7
14
|
import { create_text, get_first_child, get_last_child, next_sibling } from './operations.js';
|
|
@@ -71,12 +78,26 @@ function create_empty(anchor, render_empty) {
|
|
|
71
78
|
|
|
72
79
|
/**
|
|
73
80
|
* @param {Block} block
|
|
74
|
-
* @param {
|
|
81
|
+
* @param {ChildNode} anchor
|
|
75
82
|
* @returns {void}
|
|
76
83
|
*/
|
|
77
84
|
function move(block, anchor) {
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
// Fast path: a normal item records its own range. Only an optimized single
|
|
86
|
+
// control-flow / component root item (DOM rendered through a descendant
|
|
87
|
+
// block, `s.start` null) needs the descent via `get_first_node`/`get_last_node`.
|
|
88
|
+
var s = block.s;
|
|
89
|
+
var node = s.start;
|
|
90
|
+
var end;
|
|
91
|
+
|
|
92
|
+
if (node === null) {
|
|
93
|
+
node = get_first_node(block);
|
|
94
|
+
if (node === null) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
end = get_last_node(block);
|
|
98
|
+
} else {
|
|
99
|
+
end = s.end;
|
|
100
|
+
}
|
|
80
101
|
|
|
81
102
|
if (node === end) {
|
|
82
103
|
anchor.before(node);
|
|
@@ -87,12 +108,43 @@ function move(block, anchor) {
|
|
|
87
108
|
anchor.before(node);
|
|
88
109
|
node = next_node;
|
|
89
110
|
if (node === end) {
|
|
90
|
-
anchor.before(end);
|
|
111
|
+
anchor.before(/** @type {Node} */ (end));
|
|
91
112
|
break;
|
|
92
113
|
}
|
|
93
114
|
}
|
|
94
115
|
}
|
|
95
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the insertion anchor for a block at `index`: the first real DOM node
|
|
119
|
+
* at or after `index`, or `fallback` when every remaining block renders nothing.
|
|
120
|
+
* Scanning forward keeps insertions correct even when an optimized item renders
|
|
121
|
+
* no DOM (e.g. a single `@if` that is currently false), which previously relied
|
|
122
|
+
* on a synthesized `<!>` wrapper as a stable position marker.
|
|
123
|
+
* @param {Block[]} blocks
|
|
124
|
+
* @param {number} index
|
|
125
|
+
* @param {number} length
|
|
126
|
+
* @param {Element | Text} fallback
|
|
127
|
+
* @returns {ChildNode}
|
|
128
|
+
*/
|
|
129
|
+
function block_start(blocks, index, length, fallback) {
|
|
130
|
+
if (index >= length) {
|
|
131
|
+
return fallback;
|
|
132
|
+
}
|
|
133
|
+
// Fast path: a normal item records its own boundary, so this is the same
|
|
134
|
+
// single property read the pre-#1307 code did — no descent, no scan.
|
|
135
|
+
var first = blocks[index].s.start;
|
|
136
|
+
if (first !== null) {
|
|
137
|
+
return first;
|
|
138
|
+
}
|
|
139
|
+
for (var k = index; k < length; k++) {
|
|
140
|
+
var node = get_first_node(blocks[k]);
|
|
141
|
+
if (node !== null) {
|
|
142
|
+
return /** @type {ChildNode} */ (node);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
|
|
96
148
|
/**
|
|
97
149
|
* @template V
|
|
98
150
|
* @param {V[] | Iterable<V>} collection
|
|
@@ -429,7 +481,7 @@ function reconcile_by_key(
|
|
|
429
481
|
if (j <= b_end) {
|
|
430
482
|
while (j <= b_end) {
|
|
431
483
|
b_val = b[j];
|
|
432
|
-
var target = j
|
|
484
|
+
var target = block_start(a_blocks, j, a_length, anchor);
|
|
433
485
|
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, true);
|
|
434
486
|
j++;
|
|
435
487
|
}
|
|
@@ -548,14 +600,14 @@ function reconcile_by_key(
|
|
|
548
600
|
b_val = b[pos];
|
|
549
601
|
next_pos = pos + 1;
|
|
550
602
|
|
|
551
|
-
var target = next_pos
|
|
603
|
+
var target = block_start(b_blocks, next_pos, b_length, anchor);
|
|
552
604
|
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
|
|
553
605
|
} else if (j < 0 || i !== seq[j]) {
|
|
554
606
|
pos = i + b_start;
|
|
555
607
|
b_val = b[pos];
|
|
556
608
|
next_pos = pos + 1;
|
|
557
609
|
|
|
558
|
-
var target = next_pos
|
|
610
|
+
var target = block_start(b_blocks, next_pos, b_length, anchor);
|
|
559
611
|
move(b_blocks[pos], target);
|
|
560
612
|
} else {
|
|
561
613
|
j--;
|
|
@@ -568,7 +620,7 @@ function reconcile_by_key(
|
|
|
568
620
|
b_val = b[pos];
|
|
569
621
|
next_pos = pos + 1;
|
|
570
622
|
|
|
571
|
-
var target = next_pos
|
|
623
|
+
var target = block_start(b_blocks, next_pos, b_length, anchor);
|
|
572
624
|
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
|
|
573
625
|
}
|
|
574
626
|
}
|
|
@@ -716,7 +768,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
716
768
|
if (j <= b_end) {
|
|
717
769
|
while (j <= b_end) {
|
|
718
770
|
b_val = b[j];
|
|
719
|
-
var target = j
|
|
771
|
+
var target = block_start(a_blocks, j, a_length, anchor);
|
|
720
772
|
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, false);
|
|
721
773
|
j++;
|
|
722
774
|
}
|
|
@@ -829,14 +881,14 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
829
881
|
b_val = b[pos];
|
|
830
882
|
next_pos = pos + 1;
|
|
831
883
|
|
|
832
|
-
var target = next_pos
|
|
884
|
+
var target = block_start(b_blocks, next_pos, b_length, anchor);
|
|
833
885
|
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
|
|
834
886
|
} else if (j < 0 || i !== seq[j]) {
|
|
835
887
|
pos = i + b_start;
|
|
836
888
|
b_val = b[pos];
|
|
837
889
|
next_pos = pos + 1;
|
|
838
890
|
|
|
839
|
-
var target = next_pos
|
|
891
|
+
var target = block_start(b_blocks, next_pos, b_length, anchor);
|
|
840
892
|
move(b_blocks[pos], target);
|
|
841
893
|
} else {
|
|
842
894
|
j--;
|
|
@@ -849,7 +901,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
|
|
|
849
901
|
b_val = b[pos];
|
|
850
902
|
next_pos = pos + 1;
|
|
851
903
|
|
|
852
|
-
var target = next_pos
|
|
904
|
+
var target = block_start(b_blocks, next_pos, b_length, anchor);
|
|
853
905
|
b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
|
|
854
906
|
}
|
|
855
907
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @import { Block } from '#client' */
|
|
2
2
|
|
|
3
|
-
import { branch, destroy_block, render } from './blocks.js';
|
|
3
|
+
import { branch, destroy_block, get_first_node, get_last_node, render } from './blocks.js';
|
|
4
4
|
import { SWITCH_BLOCK } from './constants.js';
|
|
5
5
|
import { hydrate_next, hydrate_node, hydrating } from './hydration.js';
|
|
6
6
|
import { next_sibling } from './operations.js';
|
|
@@ -13,8 +13,22 @@ import { append } from './template.js';
|
|
|
13
13
|
* @returns {void}
|
|
14
14
|
*/
|
|
15
15
|
function move(block, anchor) {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Fast path: a normal case records its own range. Only an optimized single
|
|
17
|
+
// control-flow / component root case (DOM rendered through a descendant
|
|
18
|
+
// block, `s.start` null) needs the descent via `get_first_node`/`get_last_node`.
|
|
19
|
+
var s = block.s;
|
|
20
|
+
var node = s !== null ? s.start : null;
|
|
21
|
+
var end;
|
|
22
|
+
|
|
23
|
+
if (node === null) {
|
|
24
|
+
node = get_first_node(block);
|
|
25
|
+
if (node === null) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
end = get_last_node(block);
|
|
29
|
+
} else {
|
|
30
|
+
end = s.end;
|
|
31
|
+
}
|
|
18
32
|
/** @type {Node | null} */
|
|
19
33
|
var sibling;
|
|
20
34
|
|
|
@@ -13,6 +13,28 @@ describe('basic client > rendering & text', () => {
|
|
|
13
13
|
expect(container).toMatchSnapshot();
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
it('renders a bare `@` text child as literal text', () => {
|
|
17
|
+
function Basic() @{
|
|
18
|
+
<>@</>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render(Basic);
|
|
22
|
+
|
|
23
|
+
expect(container.textContent).toBe('@');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders nothing for a whitespace-only fragment', () => {
|
|
27
|
+
function Basic() @{
|
|
28
|
+
// prettier-ignore
|
|
29
|
+
<>
|
|
30
|
+
</>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render(Basic);
|
|
34
|
+
|
|
35
|
+
expect(container.textContent).toBe('');
|
|
36
|
+
});
|
|
37
|
+
|
|
16
38
|
it('renders semi-dynamic text', () => {
|
|
17
39
|
function Basic() @{
|
|
18
40
|
let message = 'Hello World';
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { RippleArray, flushSync, track } from 'ripple';
|
|
2
|
+
|
|
3
|
+
// Regression tests for the 0.3.85 optimization (PR #1307) that elides the
|
|
4
|
+
// `<!>` wrapper for single control-flow / component root scopes. When such a
|
|
5
|
+
// scope renders its DOM through a descendant block, the owning block's
|
|
6
|
+
// `s.start` is null, so keyed-for reconciliation must resolve the real first
|
|
7
|
+
// node by descending child blocks instead of reading `s.start` directly.
|
|
8
|
+
// Previously this threw `Cannot use 'in' operator to search for 'parent' in
|
|
9
|
+
// null` from `append()` on a reactive insert.
|
|
10
|
+
|
|
11
|
+
describe('keyed for with single-root item bodies (#1307 regression)', () => {
|
|
12
|
+
it(
|
|
13
|
+
'prepends into a keyed @for whose body is a single component (single control-flow root)',
|
|
14
|
+
() => {
|
|
15
|
+
function MuzeCard(props) @{
|
|
16
|
+
@if (true) {
|
|
17
|
+
<p>{props.muze.muzeId}</p>
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function App() @{
|
|
21
|
+
const muzes = new RippleArray({ muzeId: 'b' }, { muzeId: 'c' });
|
|
22
|
+
<>
|
|
23
|
+
<button onClick={() => muzes.unshift({ muzeId: 'a' })}>{'add'}</button>
|
|
24
|
+
@for (const muze of muzes; key muze.muzeId) {
|
|
25
|
+
<MuzeCard {muze} />
|
|
26
|
+
}
|
|
27
|
+
</>
|
|
28
|
+
}
|
|
29
|
+
render(App);
|
|
30
|
+
expect(container.querySelectorAll('p').length).toBe(2);
|
|
31
|
+
|
|
32
|
+
container.querySelector('button').click();
|
|
33
|
+
flushSync();
|
|
34
|
+
|
|
35
|
+
expect(Array.from(container.querySelectorAll('p')).map((p) => p.textContent)).toEqual([
|
|
36
|
+
'a',
|
|
37
|
+
'b',
|
|
38
|
+
'c',
|
|
39
|
+
]);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
it('reorders a keyed @for of single-control-flow-root components', () => {
|
|
44
|
+
function MuzeCard(props) @{
|
|
45
|
+
@if (true) {
|
|
46
|
+
<p>{props.muze.muzeId}</p>
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function App() @{
|
|
50
|
+
const muzes = new RippleArray({ muzeId: 'a' }, { muzeId: 'b' }, { muzeId: 'c' });
|
|
51
|
+
<>
|
|
52
|
+
<button onClick={() => muzes.reverse()}>{'reverse'}</button>
|
|
53
|
+
@for (const muze of muzes; key muze.muzeId) {
|
|
54
|
+
<MuzeCard {muze} />
|
|
55
|
+
}
|
|
56
|
+
</>
|
|
57
|
+
}
|
|
58
|
+
render(App);
|
|
59
|
+
|
|
60
|
+
container.querySelector('button').click();
|
|
61
|
+
flushSync();
|
|
62
|
+
|
|
63
|
+
expect(Array.from(container.querySelectorAll('p')).map((p) => p.textContent)).toEqual([
|
|
64
|
+
'c',
|
|
65
|
+
'b',
|
|
66
|
+
'a',
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('inserts in the middle of a keyed @for whose item body is a single @for', () => {
|
|
71
|
+
function App() @{
|
|
72
|
+
const groups = new RippleArray({ id: 'g1', items: ['1'] }, { id: 'g3', items: ['3'] });
|
|
73
|
+
<>
|
|
74
|
+
<button onClick={() => groups.splice(1, 0, { id: 'g2', items: ['2'] })}>{'insert'}</button>
|
|
75
|
+
@for (const group of groups; key group.id) {
|
|
76
|
+
@for (const item of group.items; key item) {
|
|
77
|
+
<p>{item}</p>
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
</>
|
|
81
|
+
}
|
|
82
|
+
render(App);
|
|
83
|
+
|
|
84
|
+
container.querySelector('button').click();
|
|
85
|
+
flushSync();
|
|
86
|
+
|
|
87
|
+
expect(Array.from(container.querySelectorAll('p')).map((p) => p.textContent)).toEqual([
|
|
88
|
+
'1',
|
|
89
|
+
'2',
|
|
90
|
+
'3',
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('realistic shape: keyed @for of components inside an @if > fragment', () => {
|
|
95
|
+
function MuzeCard(props) @{
|
|
96
|
+
@if (true) {
|
|
97
|
+
<p class="muze">{props.muze.muzeId}</p>
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function App() @{
|
|
101
|
+
let &[loaded, loadedT] = track(true);
|
|
102
|
+
const muzes = new RippleArray({ muzeId: 'b' }, { muzeId: 'c' });
|
|
103
|
+
<>
|
|
104
|
+
<button onClick={() => muzes.unshift({ muzeId: 'a' })}>{'add'}</button>
|
|
105
|
+
@if (loaded) {
|
|
106
|
+
<>
|
|
107
|
+
@for (const muze of muzes; key muze.muzeId) {
|
|
108
|
+
<MuzeCard {muze} />
|
|
109
|
+
}
|
|
110
|
+
@if (muzes.length > 0) {
|
|
111
|
+
<span class="count">{String(muzes.length)}</span>
|
|
112
|
+
}
|
|
113
|
+
</>
|
|
114
|
+
}
|
|
115
|
+
</>
|
|
116
|
+
}
|
|
117
|
+
render(App);
|
|
118
|
+
expect(container.querySelectorAll('.muze').length).toBe(2);
|
|
119
|
+
|
|
120
|
+
container.querySelector('button').click();
|
|
121
|
+
flushSync();
|
|
122
|
+
|
|
123
|
+
expect(Array.from(container.querySelectorAll('.muze')).map((p) => p.textContent)).toEqual([
|
|
124
|
+
'a',
|
|
125
|
+
'b',
|
|
126
|
+
'c',
|
|
127
|
+
]);
|
|
128
|
+
expect(container.querySelector('.count').textContent).toBe('3');
|
|
129
|
+
});
|
|
130
|
+
});
|