ripple 0.2.185 → 0.2.187
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 +2 -2
- package/src/compiler/index.d.ts +11 -2
- package/src/compiler/phases/1-parse/index.js +8 -13
- package/src/compiler/phases/2-analyze/css-analyze.js +2 -2
- package/src/compiler/phases/2-analyze/index.js +1 -16
- package/src/compiler/phases/2-analyze/prune.js +48 -3
- package/src/compiler/phases/3-transform/client/index.js +35 -23
- package/src/compiler/phases/3-transform/segments.js +313 -62
- package/src/compiler/phases/3-transform/stylesheet.js +17 -15
- package/src/compiler/source-map-utils.js +139 -65
- package/src/compiler/types/index.d.ts +22 -0
- package/src/compiler/types/parse.d.ts +6 -0
- package/src/compiler/utils.js +4 -2
- package/src/runtime/internal/client/switch.js +60 -16
- package/tests/client/switch.test.ripple +138 -2
- package/tests/server/switch.test.ripple +44 -1
|
@@ -1,9 +1,56 @@
|
|
|
1
1
|
import { decode } from '@jridgewell/sourcemap-codec';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
@import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js';
|
|
5
|
+
@import * as AST from 'estree';
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
@typedef {{
|
|
10
|
+
line: number,
|
|
11
|
+
column: number,
|
|
12
|
+
end_line: number,
|
|
13
|
+
end_column: number,
|
|
14
|
+
code: string,
|
|
15
|
+
metadata: {
|
|
16
|
+
css?: AST.Element['metadata']['css']
|
|
17
|
+
},
|
|
18
|
+
}} CodePosition
|
|
19
|
+
|
|
20
|
+
@typedef {Map<string, CodePosition[]>} CodeToGeneratedMap
|
|
21
|
+
@typedef {Map<string, {line: number, column: number}[]>} GeneratedToSourceMap
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert byte offset to line/column
|
|
26
|
+
* @param {number} offset
|
|
27
|
+
* @param {LineOffsets} line_offsets
|
|
28
|
+
* @returns {{ line: number, column: number }}
|
|
29
|
+
*/
|
|
30
|
+
export const offset_to_line_col = (offset, line_offsets) => {
|
|
31
|
+
// Binary search
|
|
32
|
+
let left = 0;
|
|
33
|
+
let right = line_offsets.length - 1;
|
|
34
|
+
let line = 1;
|
|
35
|
+
|
|
36
|
+
while (left <= right) {
|
|
37
|
+
const mid = Math.floor((left + right) / 2);
|
|
38
|
+
if (
|
|
39
|
+
offset >= line_offsets[mid] &&
|
|
40
|
+
(mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])
|
|
41
|
+
) {
|
|
42
|
+
line = mid + 1;
|
|
43
|
+
break;
|
|
44
|
+
} else if (offset < line_offsets[mid]) {
|
|
45
|
+
right = mid - 1;
|
|
46
|
+
} else {
|
|
47
|
+
left = mid + 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
4
50
|
|
|
5
|
-
|
|
6
|
-
|
|
51
|
+
const column = offset - line_offsets[line - 1];
|
|
52
|
+
return { line, column };
|
|
53
|
+
};
|
|
7
54
|
|
|
8
55
|
/**
|
|
9
56
|
* Build a source-to-generated position lookup map from an esrap source map
|
|
@@ -11,11 +58,19 @@ import { decode } from '@jridgewell/sourcemap-codec';
|
|
|
11
58
|
* @param {object} source_map - The source map object from esrap (v3 format)
|
|
12
59
|
* @param {PostProcessingChanges} post_processing_changes - Optional post-processing changes to apply
|
|
13
60
|
* @param {LineOffsets} line_offsets - Pre-computed line offsets array
|
|
14
|
-
* @
|
|
61
|
+
* @param {string} generated_code - The final generated code (after post-processing)
|
|
62
|
+
* @returns {[CodeToGeneratedMap, GeneratedToSourceMap]} Tuple of [source-to-generated map, generated-to-source map]
|
|
15
63
|
*/
|
|
16
|
-
export function
|
|
17
|
-
|
|
64
|
+
export function build_src_to_gen_map(
|
|
65
|
+
source_map,
|
|
66
|
+
post_processing_changes,
|
|
67
|
+
line_offsets,
|
|
68
|
+
generated_code,
|
|
69
|
+
) {
|
|
70
|
+
/** @type {CodeToGeneratedMap} */
|
|
18
71
|
const map = new Map();
|
|
72
|
+
/** @type {GeneratedToSourceMap} */
|
|
73
|
+
const reverse_map = new Map();
|
|
19
74
|
|
|
20
75
|
// Decode the VLQ-encoded mappings string
|
|
21
76
|
// @ts-ignore
|
|
@@ -31,101 +86,120 @@ export function build_source_to_generated_map(source_map, post_processing_change
|
|
|
31
86
|
return line_offsets[line - 1] + column;
|
|
32
87
|
};
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* @returns {{ line: number, column: number }}
|
|
38
|
-
*/
|
|
39
|
-
const offset_to_line_col = (offset) => {
|
|
40
|
-
// Binary search
|
|
41
|
-
let left = 0;
|
|
42
|
-
let right = line_offsets.length - 1;
|
|
43
|
-
let line = 1;
|
|
44
|
-
|
|
45
|
-
while (left <= right) {
|
|
46
|
-
const mid = Math.floor((left + right) / 2);
|
|
47
|
-
if (
|
|
48
|
-
offset >= line_offsets[mid] &&
|
|
49
|
-
(mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])
|
|
50
|
-
) {
|
|
51
|
-
line = mid + 1;
|
|
52
|
-
break;
|
|
53
|
-
} else if (offset < line_offsets[mid]) {
|
|
54
|
-
right = mid - 1;
|
|
55
|
-
} else {
|
|
56
|
-
left = mid + 1;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const column = offset - line_offsets[line - 1];
|
|
61
|
-
return { line, column };
|
|
62
|
-
};
|
|
89
|
+
// Apply post-processing adjustments to all segments first
|
|
90
|
+
/** @type {Array<Array<{line: number, column: number, sourceLine: number, sourceColumn: number}>>} */
|
|
91
|
+
const adjusted_segments = [];
|
|
63
92
|
|
|
64
|
-
// decoded is an array of lines, each line is an array of segments
|
|
65
|
-
// Each segment is [generatedColumn, sourceIndex, sourceLine, sourceColumn, nameIndex?]
|
|
66
93
|
for (let generated_line = 0; generated_line < decoded.length; generated_line++) {
|
|
67
94
|
const line = decoded[generated_line];
|
|
95
|
+
adjusted_segments[generated_line] = [];
|
|
68
96
|
|
|
69
97
|
for (const segment of line) {
|
|
70
98
|
if (segment.length >= 4) {
|
|
71
|
-
let generated_column = segment[0];
|
|
72
|
-
// just keeping this unused for context
|
|
73
|
-
// const source_index = segment[1]; // which source file (we only have one)
|
|
74
|
-
const source_line = /** @type {number} */ (segment[2]);
|
|
75
|
-
const source_column = /** @type {number} */ (segment[3]);
|
|
76
|
-
|
|
77
|
-
// Apply post-processing adjustments if needed
|
|
78
99
|
let adjusted_line = generated_line + 1;
|
|
79
|
-
let adjusted_column =
|
|
100
|
+
let adjusted_column = segment[0];
|
|
80
101
|
|
|
81
102
|
if (post_processing_changes) {
|
|
82
103
|
const line_change = post_processing_changes.get(adjusted_line);
|
|
83
104
|
|
|
84
105
|
if (line_change) {
|
|
85
|
-
// Check if this position is affected by the change
|
|
86
106
|
const pos_offset = line_col_to_byte_offset(adjusted_line, adjusted_column);
|
|
87
107
|
|
|
88
108
|
if (pos_offset >= line_change.offset) {
|
|
89
|
-
// Position is on or after the change - apply delta
|
|
90
109
|
const adjusted_offset = pos_offset + line_change.delta;
|
|
91
|
-
const adjusted_pos = offset_to_line_col(adjusted_offset);
|
|
110
|
+
const adjusted_pos = offset_to_line_col(adjusted_offset, line_offsets);
|
|
92
111
|
adjusted_line = adjusted_pos.line;
|
|
93
112
|
adjusted_column = adjusted_pos.column;
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
adjusted_segments[generated_line].push({
|
|
118
|
+
line: adjusted_line,
|
|
119
|
+
column: adjusted_column,
|
|
120
|
+
sourceLine: /** @type {number} */ (segment[2]),
|
|
121
|
+
sourceColumn: /** @type {number} */ (segment[3]),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Now build the map using adjusted positions
|
|
128
|
+
for (let line_idx = 0; line_idx < adjusted_segments.length; line_idx++) {
|
|
129
|
+
const line_segments = adjusted_segments[line_idx];
|
|
130
|
+
|
|
131
|
+
for (let seg_idx = 0; seg_idx < line_segments.length; seg_idx++) {
|
|
132
|
+
const segment = line_segments[seg_idx];
|
|
133
|
+
const line = segment.line;
|
|
134
|
+
const column = segment.column;
|
|
135
|
+
|
|
136
|
+
// Determine end position using next segment
|
|
137
|
+
let end_line = line;
|
|
138
|
+
let end_column = column;
|
|
139
|
+
|
|
140
|
+
// Look for next segment to determine end position
|
|
141
|
+
if (seg_idx + 1 < line_segments.length) {
|
|
142
|
+
// Next segment on same line
|
|
143
|
+
const next_segment = line_segments[seg_idx + 1];
|
|
144
|
+
end_line = next_segment.line;
|
|
145
|
+
end_column = next_segment.column;
|
|
146
|
+
} else if (
|
|
147
|
+
line_idx + 1 < adjusted_segments.length &&
|
|
148
|
+
adjusted_segments[line_idx + 1].length > 0
|
|
149
|
+
) {
|
|
150
|
+
// Look at first segment of next line
|
|
151
|
+
const next_segment = adjusted_segments[line_idx + 1][0];
|
|
152
|
+
end_line = next_segment.line;
|
|
153
|
+
end_column = next_segment.column;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Extract code snippet
|
|
157
|
+
const start_offset = line_col_to_byte_offset(line, column);
|
|
158
|
+
const end_offset = line_col_to_byte_offset(end_line, end_column);
|
|
159
|
+
const code_snippet = generated_code.slice(start_offset, end_offset);
|
|
100
160
|
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
// Create key from source position (1-indexed line, 0-indexed column)
|
|
162
|
+
segment.sourceLine += 1;
|
|
163
|
+
const key = `${segment.sourceLine}:${segment.sourceColumn}`;
|
|
103
164
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
// Store adjusted generated position with code snippet
|
|
166
|
+
const gen_pos = { line, column, end_line, end_column, code: code_snippet, metadata: {} };
|
|
167
|
+
|
|
168
|
+
if (!map.has(key)) {
|
|
169
|
+
map.set(key, []);
|
|
170
|
+
}
|
|
171
|
+
/** @type {CodePosition[]} */ (map.get(key)).push(gen_pos);
|
|
172
|
+
|
|
173
|
+
// Store reverse mapping (generated to source)
|
|
174
|
+
const gen_key = `${gen_pos.line}:${gen_pos.column}`;
|
|
175
|
+
|
|
176
|
+
if (!reverse_map.has(gen_key)) {
|
|
177
|
+
reverse_map.set(gen_key, []);
|
|
108
178
|
}
|
|
179
|
+
reverse_map.get(gen_key)?.push({
|
|
180
|
+
line: segment.sourceLine,
|
|
181
|
+
column: segment.sourceColumn,
|
|
182
|
+
});
|
|
109
183
|
}
|
|
110
184
|
}
|
|
111
185
|
|
|
112
|
-
return map;
|
|
186
|
+
return [map, reverse_map];
|
|
113
187
|
}
|
|
114
188
|
|
|
115
189
|
/**
|
|
116
190
|
* Look up generated position for a given source position
|
|
117
|
-
* @param {number}
|
|
118
|
-
* @param {number}
|
|
119
|
-
* @param {
|
|
120
|
-
* @returns {
|
|
191
|
+
* @param {number} src_line - 1-based line number in source
|
|
192
|
+
* @param {number} src_column - 0-based column number in source
|
|
193
|
+
* @param {CodeToGeneratedMap} src_to_gen_map - Lookup map
|
|
194
|
+
* @returns {CodePosition} Generated position
|
|
121
195
|
*/
|
|
122
|
-
export function get_generated_position(
|
|
123
|
-
const key = `${
|
|
124
|
-
const positions =
|
|
196
|
+
export function get_generated_position(src_line, src_column, src_to_gen_map) {
|
|
197
|
+
const key = `${src_line}:${src_column}`;
|
|
198
|
+
const positions = src_to_gen_map.get(key);
|
|
125
199
|
|
|
126
200
|
if (!positions || positions.length === 0) {
|
|
127
201
|
// No mapping found in source map - this shouldn't happen since all tokens should have mappings
|
|
128
|
-
throw new Error(`No source map entry for position "${
|
|
202
|
+
throw new Error(`No source map entry for position "${src_line}:${src_column}"`);
|
|
129
203
|
}
|
|
130
204
|
|
|
131
205
|
// If multiple generated positions map to same source, return the first
|
|
@@ -226,6 +226,28 @@ declare module 'estree' {
|
|
|
226
226
|
loc: SourceLocation;
|
|
227
227
|
metadata: BaseNodeMetaData & {
|
|
228
228
|
ts_name?: string;
|
|
229
|
+
// for <style> tag
|
|
230
|
+
styleScopeHash?: string;
|
|
231
|
+
// for elements with scoped style classes
|
|
232
|
+
css?: {
|
|
233
|
+
scopedClasses: Map<
|
|
234
|
+
string,
|
|
235
|
+
{
|
|
236
|
+
start: number;
|
|
237
|
+
end: number;
|
|
238
|
+
selector: CSS.ClassSelector;
|
|
239
|
+
}
|
|
240
|
+
>;
|
|
241
|
+
topScopedClasses: Map<
|
|
242
|
+
string,
|
|
243
|
+
{
|
|
244
|
+
start: number;
|
|
245
|
+
end: number;
|
|
246
|
+
selector: CSS.ClassSelector;
|
|
247
|
+
}
|
|
248
|
+
>;
|
|
249
|
+
hash: string;
|
|
250
|
+
};
|
|
229
251
|
};
|
|
230
252
|
|
|
231
253
|
// currently only for <style> and <script> tags
|
|
@@ -56,6 +56,12 @@ declare module 'zimmerframe' {
|
|
|
56
56
|
state: any,
|
|
57
57
|
visitors: RippleCompiler.Visitors<AST.Node, any>,
|
|
58
58
|
): AST.Node;
|
|
59
|
+
|
|
60
|
+
export function walk(
|
|
61
|
+
node: AST.CSS.Node,
|
|
62
|
+
state: any,
|
|
63
|
+
visitors: RippleCompiler.Visitors<(AST.CSS.Node), any>,
|
|
64
|
+
): AST.CSS.Node;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
export namespace Parse {
|
package/src/compiler/utils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
@import * as AST from 'estree';
|
|
3
|
+
@import { CommonContext, NameSpace, ScopeInterface } from '#compiler';
|
|
4
|
+
*/
|
|
3
5
|
|
|
4
6
|
import { build_assignment_value, extract_paths } from '../utils/ast.js';
|
|
5
7
|
import * as b from '../utils/builders.js';
|
|
@@ -2,32 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
import { branch, destroy_block, render } from './blocks.js';
|
|
4
4
|
import { SWITCH_BLOCK } from './constants.js';
|
|
5
|
+
import { next_sibling } from './operations.js';
|
|
6
|
+
import { append } from './template.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @param {
|
|
9
|
+
* Moves a block's DOM nodes to before an anchor node
|
|
10
|
+
* @param {Block} block
|
|
11
|
+
* @param {ChildNode} anchor
|
|
9
12
|
* @returns {void}
|
|
10
13
|
*/
|
|
11
|
-
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
function move(block, anchor) {
|
|
15
|
+
var node = block.s.start;
|
|
16
|
+
var end = block.s.end;
|
|
17
|
+
/** @type {Node | null} */
|
|
18
|
+
var sibling;
|
|
19
|
+
|
|
20
|
+
while (node !== null) {
|
|
21
|
+
if (node === end) {
|
|
22
|
+
append(anchor, node);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
sibling = next_sibling(node);
|
|
26
|
+
append(anchor, node);
|
|
27
|
+
node = sibling;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {ChildNode} anchor
|
|
33
|
+
* @param {() => ((anchor: ChildNode) => void)[] | null} fn
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
export function switch_block(anchor, fn) {
|
|
37
|
+
/** @type {((anchor: ChildNode) => void)[]} */
|
|
38
|
+
var prev = [];
|
|
39
|
+
/** @type {Map<(anchor: ChildNode) => void, Block>} */
|
|
40
|
+
var blocks = new Map();
|
|
17
41
|
|
|
18
42
|
render(
|
|
19
43
|
() => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
current_branch = branch_fn;
|
|
44
|
+
var funcs = fn();
|
|
45
|
+
let same = prev.length === funcs?.length || false;
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
for (var i = 0; i < prev.length; i++) {
|
|
48
|
+
var p = prev[i];
|
|
49
|
+
|
|
50
|
+
if (!funcs || funcs.indexOf(p) === -1) {
|
|
51
|
+
same = false;
|
|
52
|
+
destroy_block(/** @type {Block} */ (blocks.get(p)));
|
|
53
|
+
blocks.delete(p);
|
|
54
|
+
}
|
|
27
55
|
}
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
prev = funcs ?? [];
|
|
58
|
+
|
|
59
|
+
if (same || !funcs) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (var i = 0; i < funcs.length; i++) {
|
|
64
|
+
var n = funcs[i];
|
|
65
|
+
var b = blocks.get(n);
|
|
66
|
+
if (b) {
|
|
67
|
+
move(b, anchor);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
blocks.set(
|
|
72
|
+
n,
|
|
73
|
+
branch(() => n(anchor)),
|
|
74
|
+
);
|
|
31
75
|
}
|
|
32
76
|
},
|
|
33
77
|
null,
|
|
@@ -89,7 +89,7 @@ describe('switch statements', () => {
|
|
|
89
89
|
expect(container.querySelector('div').textContent).toBe('Default for y');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('renders switch using
|
|
92
|
+
it('renders switch using empty case fall-through', () => {
|
|
93
93
|
component App() {
|
|
94
94
|
let value = track('a');
|
|
95
95
|
|
|
@@ -131,7 +131,7 @@ describe('switch statements', () => {
|
|
|
131
131
|
expect(container.querySelector('div').textContent).toBe('DOM check');
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
it('renders switch with template content and
|
|
134
|
+
it('renders switch with template content and reacts to tracked changes', () => {
|
|
135
135
|
component App() {
|
|
136
136
|
let status = track('active');
|
|
137
137
|
let message = track('');
|
|
@@ -180,4 +180,140 @@ describe('switch statements', () => {
|
|
|
180
180
|
expect(container.querySelector('div').textContent).toBe('Status: An error occurred.');
|
|
181
181
|
expect(container.querySelector('.error')).toBeTruthy();
|
|
182
182
|
});
|
|
183
|
+
|
|
184
|
+
it(
|
|
185
|
+
'renders switch with multiple non-empty fall-through cases and reacts to tracked changes without recreating DOM unnecessarily',
|
|
186
|
+
() => {
|
|
187
|
+
component App() {
|
|
188
|
+
let status = track(0);
|
|
189
|
+
<div>
|
|
190
|
+
switch (@status) {
|
|
191
|
+
case -1:
|
|
192
|
+
case 0:
|
|
193
|
+
<p>{' Idle'}</p>
|
|
194
|
+
case 1:
|
|
195
|
+
<p>{' Loading'}</p>
|
|
196
|
+
case 2:
|
|
197
|
+
<p>{' Success'}</p>
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
<p>{' Unknown status'}</p>
|
|
201
|
+
<p>{' Unknown 2'}</p>
|
|
202
|
+
case 3:
|
|
203
|
+
<p>{' Error'}</p>
|
|
204
|
+
<p>{' Error 2'}</p>
|
|
205
|
+
<p>{' Error 3'}</p>
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
</div>
|
|
209
|
+
<button
|
|
210
|
+
onClick={() => {
|
|
211
|
+
@status = (@status + 1) % 5;
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{'Next Status'}
|
|
215
|
+
</button>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
render(App);
|
|
219
|
+
const button = container.querySelector('button');
|
|
220
|
+
|
|
221
|
+
expect(container.querySelector('div').textContent).toBe(' Idle Loading Success');
|
|
222
|
+
|
|
223
|
+
button.click();
|
|
224
|
+
flushSync();
|
|
225
|
+
|
|
226
|
+
expect(container.querySelector('div').textContent).toBe(' Loading Success');
|
|
227
|
+
|
|
228
|
+
button.click();
|
|
229
|
+
flushSync();
|
|
230
|
+
|
|
231
|
+
expect(container.querySelector('div').textContent).toBe(' Success');
|
|
232
|
+
|
|
233
|
+
button.click();
|
|
234
|
+
flushSync();
|
|
235
|
+
|
|
236
|
+
expect(container.querySelector('div').textContent).toBe(' Error Error 2 Error 3');
|
|
237
|
+
|
|
238
|
+
button.click();
|
|
239
|
+
flushSync();
|
|
240
|
+
|
|
241
|
+
expect(container.querySelector('div').textContent).toBe(
|
|
242
|
+
' Unknown status Unknown 2 Error Error 2 Error 3',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
button.click();
|
|
246
|
+
flushSync();
|
|
247
|
+
|
|
248
|
+
expect(container.querySelector('div').textContent).toBe(' Idle Loading Success');
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
it(
|
|
253
|
+
'renders a fall-through default in the middle of switch cases and reacts to changes without recreating DOM unnecessarily',
|
|
254
|
+
() => {
|
|
255
|
+
component App() {
|
|
256
|
+
let value = track('x');
|
|
257
|
+
|
|
258
|
+
<button onClick={() => (@value = 'a')}>{'Set A'}</button>
|
|
259
|
+
<button onClick={() => (@value = 'b')}>{'Set B'}</button>
|
|
260
|
+
<button onClick={() => (@value = 'c')}>{'Set C'}</button>
|
|
261
|
+
<button onClick={() => (@value = 'x')}>{'Set X'}</button>
|
|
262
|
+
|
|
263
|
+
<div>
|
|
264
|
+
switch (@value) {
|
|
265
|
+
case 'a':
|
|
266
|
+
<div>{' Case A'}</div>
|
|
267
|
+
break;
|
|
268
|
+
// NOTE: This should be the default in the middle of the cases
|
|
269
|
+
// However, jsdom (and other node-based dom libs) has a bug
|
|
270
|
+
// that breaks out of the switch even if the default doesn't have a break
|
|
271
|
+
// The browser works correctly.
|
|
272
|
+
// So, we're just using a defined case in the middle to simulate default.
|
|
273
|
+
case 'x':
|
|
274
|
+
<div>{' Default Case for ' + @value}</div>
|
|
275
|
+
case 'b':
|
|
276
|
+
<div>{' Case B'}</div>
|
|
277
|
+
break;
|
|
278
|
+
case 'c':
|
|
279
|
+
<div>{' Case C'}</div>
|
|
280
|
+
}
|
|
281
|
+
</div>
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
render(App);
|
|
285
|
+
const [buttonA, buttonB, buttonC, buttonX] = container.querySelectorAll('button');
|
|
286
|
+
|
|
287
|
+
expect(container.querySelector('div').textContent).toBe(' Default Case for x Case B');
|
|
288
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(2);
|
|
289
|
+
|
|
290
|
+
buttonA.click();
|
|
291
|
+
flushSync();
|
|
292
|
+
|
|
293
|
+
expect(container.querySelector('div').textContent).toBe(' Case A');
|
|
294
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(1);
|
|
295
|
+
|
|
296
|
+
buttonC.click();
|
|
297
|
+
flushSync();
|
|
298
|
+
|
|
299
|
+
expect(container.querySelector('div').textContent).toBe(' Case C');
|
|
300
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(1);
|
|
301
|
+
|
|
302
|
+
buttonB.click();
|
|
303
|
+
flushSync();
|
|
304
|
+
|
|
305
|
+
const bDiv = container.querySelector('div').querySelectorAll('div')[0];
|
|
306
|
+
expect(bDiv.textContent).toBe(' Case B');
|
|
307
|
+
bDiv.id = 'b';
|
|
308
|
+
|
|
309
|
+
buttonX.click();
|
|
310
|
+
flushSync();
|
|
311
|
+
|
|
312
|
+
// order should be correct with the previously rendered B element
|
|
313
|
+
expect(container.querySelector('div').textContent).toBe(' Default Case for x Case B');
|
|
314
|
+
expect(container.querySelector('div').querySelectorAll('div').length).toBe(2);
|
|
315
|
+
// the previously rendered div should be preserved
|
|
316
|
+
expect(container.querySelector('div').querySelectorAll('div')[1].id).toBe('b');
|
|
317
|
+
},
|
|
318
|
+
);
|
|
183
319
|
});
|
|
@@ -25,7 +25,7 @@ describe('SSR: switch statements', () => {
|
|
|
25
25
|
expect(body).toBe('<div>Case B</div>');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it('renders
|
|
28
|
+
it('renders a fall-through with an empty switch case', async () => {
|
|
29
29
|
component App() {
|
|
30
30
|
let value = 'b';
|
|
31
31
|
|
|
@@ -45,4 +45,47 @@ describe('SSR: switch statements', () => {
|
|
|
45
45
|
const { body } = await render(App);
|
|
46
46
|
expect(body).toBe('<div>Case B or C</div>');
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it('renders a fall-through with a case that has elements', async () => {
|
|
50
|
+
component App() {
|
|
51
|
+
let value = 'a';
|
|
52
|
+
|
|
53
|
+
switch (value) {
|
|
54
|
+
case 'a':
|
|
55
|
+
<div>{'Case A'}</div>
|
|
56
|
+
case 'b':
|
|
57
|
+
<div>{'Case B'}</div>
|
|
58
|
+
case 'c':
|
|
59
|
+
<div>{'Case C'}</div>
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
<div>{'Default Case'}</div>
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { body } = await render(App);
|
|
67
|
+
expect(body).toBe('<div>Case A</div><div>Case B</div><div>Case C</div>');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders a fall-through with a default case in the middle', async () => {
|
|
71
|
+
component App() {
|
|
72
|
+
let value = 'x';
|
|
73
|
+
|
|
74
|
+
switch (value) {
|
|
75
|
+
case 'a':
|
|
76
|
+
<div>{'Case A'}</div>
|
|
77
|
+
default:
|
|
78
|
+
<div>{'Default Case'}</div>
|
|
79
|
+
case 'b':
|
|
80
|
+
<div>{'Case B'}</div>
|
|
81
|
+
break;
|
|
82
|
+
case 'c':
|
|
83
|
+
<div>{'Case C'}</div>
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { body } = await render(App);
|
|
89
|
+
expect(body).toBe('<div>Default Case</div><div>Case B</div>');
|
|
90
|
+
});
|
|
48
91
|
});
|