ripple 0.2.88 → 0.2.89
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 -1
- package/src/compiler/phases/1-parse/index.js +44 -6
- package/src/compiler/phases/3-transform/client/index.js +29 -7
- package/src/runtime/array.js +9 -242
- package/src/runtime/index-client.js +3 -1
- package/src/runtime/internal/client/constants.js +2 -0
- package/src/runtime/internal/client/index.js +2 -0
- package/src/runtime/internal/client/operations.js +0 -1
- package/src/runtime/internal/client/render.js +5 -5
- package/src/runtime/internal/client/runtime.js +2 -2
- package/src/runtime/internal/client/utils.js +4 -0
- package/src/runtime/internal/server/index.js +5 -2
- package/src/runtime/object.js +29 -0
- package/src/runtime/proxy.js +341 -0
- package/src/utils/events.js +3 -3
- package/tests/client/array.test.ripple +3 -3
- package/tests/client/basic.test.ripple +75 -0
- package/tests/client/head.test.ripple +196 -0
- package/tests/client/object.test.ripple +183 -0
- package/types/index.d.ts +8 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/** @import { Block, Tracked } from '#client' */
|
|
2
|
+
/** @import { TrackedArray } from './array.js' */
|
|
3
|
+
/** @import { TrackedObject } from './object.js' */
|
|
4
|
+
|
|
5
|
+
import { get, set, tracked } from './internal/client/runtime.js';
|
|
6
|
+
import {
|
|
7
|
+
array_prototype,
|
|
8
|
+
get_descriptor,
|
|
9
|
+
get_prototype_of,
|
|
10
|
+
is_array,
|
|
11
|
+
object_prototype,
|
|
12
|
+
} from './internal/client/utils.js';
|
|
13
|
+
import {
|
|
14
|
+
MAX_ARRAY_LENGTH,
|
|
15
|
+
TRACKED_ARRAY,
|
|
16
|
+
TRACKED_OBJECT,
|
|
17
|
+
UNINITIALIZED,
|
|
18
|
+
} from './internal/client/constants.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @template T
|
|
22
|
+
* @param {T[] | Record<PropertyKey, any>} value
|
|
23
|
+
* @param {Block} block
|
|
24
|
+
* @returns {TrackedArray<T> | TrackedObject<T>}
|
|
25
|
+
*/
|
|
26
|
+
export function proxy(value, block) {
|
|
27
|
+
// if non-proxyable, or is already a proxy, return `value`
|
|
28
|
+
if (
|
|
29
|
+
typeof value !== 'object'
|
|
30
|
+
|| value === null
|
|
31
|
+
|| TRACKED_ARRAY in value
|
|
32
|
+
|| TRACKED_OBJECT in value
|
|
33
|
+
) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const prototype = get_prototype_of(value);
|
|
38
|
+
|
|
39
|
+
if (prototype !== object_prototype && prototype !== array_prototype) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var tracked_elements = new Map();
|
|
44
|
+
var is_proxied_array = is_array(value);
|
|
45
|
+
/** @type {Tracked} */
|
|
46
|
+
var tracked_len;
|
|
47
|
+
|
|
48
|
+
if (is_proxied_array) {
|
|
49
|
+
tracked_len = tracked(value.length, block);
|
|
50
|
+
tracked_elements.set('length', tracked_len);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return new Proxy(value, {
|
|
54
|
+
/**
|
|
55
|
+
* @param {PropertyKey} prop
|
|
56
|
+
*/
|
|
57
|
+
get(target, prop, receiver) {
|
|
58
|
+
var t = tracked_elements.get(prop);
|
|
59
|
+
var exists = prop in target;
|
|
60
|
+
|
|
61
|
+
if (t === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
|
|
62
|
+
t = tracked(exists ? /** @type {any} */ (target)[prop] : UNINITIALIZED, block);
|
|
63
|
+
tracked_elements.set(prop, t);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (t !== undefined) {
|
|
67
|
+
var v = get(t);
|
|
68
|
+
return v === UNINITIALIZED ? undefined : v;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var result = Reflect.get(target, prop, receiver);
|
|
72
|
+
|
|
73
|
+
if (typeof result === 'function') {
|
|
74
|
+
if (methods_returning_arrays.has(prop)) {
|
|
75
|
+
/** @type {(this: any, ...args: any[]) => any} */
|
|
76
|
+
return function (...args) {
|
|
77
|
+
var output = Reflect.apply(result, receiver, args);
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(output) && output !== target) {
|
|
80
|
+
return array_proxy({ elements: output, block, use_array: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return output;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// When generating an iterator, we need to ensure that length is tracked
|
|
88
|
+
if (is_proxied_array && (prop === 'entries' || prop === 'values' || prop === 'keys')) {
|
|
89
|
+
receiver.length;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
set(target, prop, value, receiver) {
|
|
97
|
+
var t = tracked_elements.get(prop);
|
|
98
|
+
var exists = prop in target;
|
|
99
|
+
|
|
100
|
+
if (is_proxied_array && prop === 'length') {
|
|
101
|
+
for (var i = value; i < t.v; i += 1) {
|
|
102
|
+
var other_t = tracked_elements.get(i + '');
|
|
103
|
+
if (other_t !== undefined) {
|
|
104
|
+
set(other_t, UNINITIALIZED, block);
|
|
105
|
+
} else if (i in target) {
|
|
106
|
+
// If the item exists in the original, we need to create a uninitialized tracked,
|
|
107
|
+
// else a later read of the property would result in a tracked being created with
|
|
108
|
+
// the value of the original item at that index.
|
|
109
|
+
other_t = tracked(UNINITIALIZED, block);
|
|
110
|
+
tracked_elements.set(i + '', other_t);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If we haven't yet created a tracked for this property, we need to ensure
|
|
116
|
+
// we do so otherwise if we read it later, then the write won't be tracked and
|
|
117
|
+
// the heuristics of effects will be different vs if we had read the proxied
|
|
118
|
+
// object property before writing to that property.
|
|
119
|
+
if (t === undefined) {
|
|
120
|
+
if (!exists || get_descriptor(target, prop)?.writable) {
|
|
121
|
+
t = tracked(undefined, block);
|
|
122
|
+
set(t, value, block);
|
|
123
|
+
|
|
124
|
+
tracked_elements.set(prop, t);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
exists = t.v !== UNINITIALIZED;
|
|
128
|
+
|
|
129
|
+
set(t, value, block);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
133
|
+
|
|
134
|
+
// Set the new value before updating any tracked's so that any listeners get the new value
|
|
135
|
+
if (descriptor?.set) {
|
|
136
|
+
descriptor.set.call(receiver, value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!exists && is_proxied_array && typeof prop === 'string' ) {
|
|
140
|
+
// If we have mutated an array directly, we might need to
|
|
141
|
+
// signal that length has also changed. Do it before updating metadata
|
|
142
|
+
// to ensure that iterating over the array as a result of a metadata update
|
|
143
|
+
// will not cause the length to be out of sync.
|
|
144
|
+
var n = Number(prop);
|
|
145
|
+
|
|
146
|
+
if (Number.isInteger(n) && n >= tracked_len.v) {
|
|
147
|
+
set(tracked_len, n + 1, block);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
setPrototypeOf() {
|
|
155
|
+
throw new Error(`Cannot set prototype of ${is_proxied_array ? '\`TrackedArray\`' : '\`TrackedObject\`'}`);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
deleteProperty(target, prop) {
|
|
159
|
+
var t = tracked_elements.get(prop);
|
|
160
|
+
|
|
161
|
+
if (t === undefined) {
|
|
162
|
+
if (prop in target) {
|
|
163
|
+
const t = tracked(UNINITIALIZED, block);
|
|
164
|
+
tracked_elements.set(prop, t);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
set(t, UNINITIALIZED, block);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return Reflect.deleteProperty(target, prop);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
has(target, prop) {
|
|
174
|
+
if (is_proxied_array && prop === TRACKED_ARRAY) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (prop === TRACKED_OBJECT) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
var t = tracked_elements.get(prop);
|
|
183
|
+
var exists = (t !== undefined && t.v !== UNINITIALIZED) || Reflect.has(target, prop);
|
|
184
|
+
|
|
185
|
+
if (t !== undefined || !exists || get_descriptor(target, prop)?.writable) {
|
|
186
|
+
if (t === undefined) {
|
|
187
|
+
t = tracked(exists ? /** @type {any} */ (target)[prop] : UNINITIALIZED, block);
|
|
188
|
+
|
|
189
|
+
tracked_elements.set(prop, t);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var value = get(t);
|
|
193
|
+
if (value === UNINITIALIZED) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return exists;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
defineProperty(_, prop, descriptor) {
|
|
202
|
+
if (
|
|
203
|
+
!('value' in descriptor) ||
|
|
204
|
+
descriptor.configurable === false ||
|
|
205
|
+
descriptor.enumerable === false ||
|
|
206
|
+
descriptor.writable === false
|
|
207
|
+
) {
|
|
208
|
+
// we disallow non-basic descriptors, because unless they are applied to the
|
|
209
|
+
// target object — which we avoid, so that state can be forked — we will run
|
|
210
|
+
// afoul of the various invariants
|
|
211
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants
|
|
212
|
+
throw new Error(
|
|
213
|
+
'Only basic property descriptors are supported with value and configurable, enumerable, and writable set to true',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var t = tracked_elements.get(prop);
|
|
218
|
+
|
|
219
|
+
if (t === undefined) {
|
|
220
|
+
t = tracked(descriptor.value, block);
|
|
221
|
+
tracked_elements.set(prop, t);
|
|
222
|
+
} else {
|
|
223
|
+
set(t, descriptor.value, block);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return true;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
ownKeys(target) {
|
|
230
|
+
var own_keys = Reflect.ownKeys(target).filter((key) => {
|
|
231
|
+
var t = tracked_elements.get(key);
|
|
232
|
+
return t === undefined || t.v !== UNINITIALIZED;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
for (var [key, t] of tracked_elements) {
|
|
236
|
+
if (t.v !== UNINITIALIZED && !(key in target)) {
|
|
237
|
+
own_keys.push(key);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return own_keys;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
245
|
+
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
246
|
+
|
|
247
|
+
if (descriptor && 'value' in descriptor) {
|
|
248
|
+
var t = tracked_elements.get(prop);
|
|
249
|
+
if (t) descriptor.value = get(t);
|
|
250
|
+
} else if (descriptor === undefined) {
|
|
251
|
+
var t = tracked_elements.get(prop);
|
|
252
|
+
var value = t?.v;
|
|
253
|
+
|
|
254
|
+
if (t !== undefined && value !== UNINITIALIZED) {
|
|
255
|
+
return {
|
|
256
|
+
enumerable: true,
|
|
257
|
+
configurable: true,
|
|
258
|
+
value,
|
|
259
|
+
writable: true
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return descriptor;
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @template T
|
|
272
|
+
* @param {{
|
|
273
|
+
* elements: Iterable<T>,
|
|
274
|
+
* block: Block,
|
|
275
|
+
* from_static?: boolean,
|
|
276
|
+
* use_array?: boolean
|
|
277
|
+
* }} params
|
|
278
|
+
* @returns {TrackedArray<T>}
|
|
279
|
+
*/
|
|
280
|
+
export function array_proxy({ elements, block, from_static = false, use_array = false }) {
|
|
281
|
+
var arr;
|
|
282
|
+
var first;
|
|
283
|
+
|
|
284
|
+
if (
|
|
285
|
+
from_static &&
|
|
286
|
+
(first = get_first_if_length(/** @type {Array<T>} */ (elements))) !== undefined
|
|
287
|
+
) {
|
|
288
|
+
arr = new Array();
|
|
289
|
+
arr[0] = first;
|
|
290
|
+
} else if (use_array) {
|
|
291
|
+
arr = elements;
|
|
292
|
+
} else {
|
|
293
|
+
arr = new Array(...elements);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return proxy(arr, block);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @template {object} T
|
|
301
|
+
* @param {T} obj
|
|
302
|
+
* @param {Block} block
|
|
303
|
+
* @returns {TrackedObject<T>}
|
|
304
|
+
*/
|
|
305
|
+
export function object_proxy(obj, block) {
|
|
306
|
+
return proxy(obj, block);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @type {Set<PropertyKey>} */
|
|
310
|
+
const methods_returning_arrays = new Set([
|
|
311
|
+
'concat',
|
|
312
|
+
'filter',
|
|
313
|
+
'flat',
|
|
314
|
+
'flatMap',
|
|
315
|
+
'map',
|
|
316
|
+
'slice',
|
|
317
|
+
'splice',
|
|
318
|
+
'toReversed',
|
|
319
|
+
'toSorted',
|
|
320
|
+
'toSpliced',
|
|
321
|
+
'with',
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @template T
|
|
326
|
+
* @param {Array<T>} array
|
|
327
|
+
* @returns {number | void}
|
|
328
|
+
*/
|
|
329
|
+
function get_first_if_length(array) {
|
|
330
|
+
var first = array[0];
|
|
331
|
+
|
|
332
|
+
if (
|
|
333
|
+
array.length === 1 &&
|
|
334
|
+
0 in array &&
|
|
335
|
+
Number.isInteger(first) &&
|
|
336
|
+
/** @type {number} */ (first) >= 0 &&
|
|
337
|
+
/** @type {number} */ (first) <= MAX_ARRAY_LENGTH
|
|
338
|
+
) {
|
|
339
|
+
return /** @type {number} */ (first);
|
|
340
|
+
}
|
|
341
|
+
}
|
package/src/utils/events.js
CHANGED
|
@@ -58,11 +58,11 @@ export function is_capture_event(name) {
|
|
|
58
58
|
* @param {string} event_name
|
|
59
59
|
*/
|
|
60
60
|
export function get_attribute_event_name(event_name) {
|
|
61
|
-
event_name = event_name.slice(2);
|
|
61
|
+
event_name = event_name.slice(2); // strip "on"
|
|
62
62
|
if (is_capture_event(event_name)) {
|
|
63
|
-
event_name = event_name.slice(0, -7);
|
|
63
|
+
event_name = event_name.slice(0, -7); // strip "Capture"
|
|
64
64
|
}
|
|
65
|
-
return event_name
|
|
65
|
+
return event_name.toLowerCase();
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
|
|
@@ -7,7 +7,7 @@ describe('TrackedArray', () => {
|
|
|
7
7
|
|
|
8
8
|
function render(component) {
|
|
9
9
|
mount(component, {
|
|
10
|
-
|
|
10
|
+
target: container
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -877,7 +877,7 @@ describe('TrackedArray', () => {
|
|
|
877
877
|
expect(container.querySelectorAll('pre')[1].textContent).toBe('[5,4,3,2,1]');
|
|
878
878
|
});
|
|
879
879
|
|
|
880
|
-
it('handles toSorted method with reactivity', () => {
|
|
880
|
+
it('handles toSorted method with reactivity', (context) => {
|
|
881
881
|
if (!('toSorted' in Array.prototype)) {
|
|
882
882
|
context.skip();
|
|
883
883
|
}
|
|
@@ -907,7 +907,7 @@ describe('TrackedArray', () => {
|
|
|
907
907
|
expect(container.querySelectorAll('pre')[1].textContent).toBe('[0,1,2,3,4]');
|
|
908
908
|
});
|
|
909
909
|
|
|
910
|
-
it('handles toSpliced method with reactivity', () => {
|
|
910
|
+
it('handles toSpliced method with reactivity', (context) => {
|
|
911
911
|
if (!('toSpliced' in Array.prototype)) {
|
|
912
912
|
context.skip();
|
|
913
913
|
}
|
|
@@ -142,6 +142,81 @@ describe('basic client', () => {
|
|
|
142
142
|
expect(div.classList.contains('inactive')).toBe(true);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
it('render class attribute with array, nested array, nested object', () => {
|
|
146
|
+
component Basic() {
|
|
147
|
+
<div class={[
|
|
148
|
+
'foo',
|
|
149
|
+
'bar',
|
|
150
|
+
true && 'baz',
|
|
151
|
+
false && 'aaa',
|
|
152
|
+
null && 'bbb',
|
|
153
|
+
[
|
|
154
|
+
'ccc',
|
|
155
|
+
'ddd',
|
|
156
|
+
{ eee: true, fff: false }
|
|
157
|
+
]
|
|
158
|
+
]}>
|
|
159
|
+
{'Class Array'}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<style>
|
|
163
|
+
.foo {
|
|
164
|
+
color: red;
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
render(Basic);
|
|
170
|
+
|
|
171
|
+
const div = container.querySelector('div');
|
|
172
|
+
|
|
173
|
+
expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
|
|
174
|
+
expect(div.classList.contains('foo')).toBe(true);
|
|
175
|
+
expect(div.classList.contains('bar')).toBe(true);
|
|
176
|
+
expect(div.classList.contains('baz')).toBe(true);
|
|
177
|
+
expect(div.classList.contains('aaa')).toBe(false);
|
|
178
|
+
expect(div.classList.contains('bbb')).toBe(false);
|
|
179
|
+
expect(div.classList.contains('ccc')).toBe(true);
|
|
180
|
+
expect(div.classList.contains('ddd')).toBe(true);
|
|
181
|
+
expect(div.classList.contains('eee')).toBe(true);
|
|
182
|
+
expect(div.classList.contains('fff')).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('render dynamic class object', () => {
|
|
186
|
+
component Basic() {
|
|
187
|
+
let active = track(false);
|
|
188
|
+
|
|
189
|
+
<button onClick={() => { @active = !@active }}>{'Toggle'}</button>
|
|
190
|
+
<div class={{ active: @active, inactive: !@active }}>{'Dynamic Class'}</div>
|
|
191
|
+
|
|
192
|
+
<style>
|
|
193
|
+
.active {
|
|
194
|
+
color: green;
|
|
195
|
+
}
|
|
196
|
+
</style>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
render(Basic);
|
|
200
|
+
|
|
201
|
+
const button = container.querySelector('button');
|
|
202
|
+
const div = container.querySelector('div');
|
|
203
|
+
|
|
204
|
+
expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
|
|
205
|
+
expect(div.classList.contains('inactive')).toBe(true);
|
|
206
|
+
expect(div.classList.contains('active')).toBe(false);
|
|
207
|
+
|
|
208
|
+
button.click();
|
|
209
|
+
flushSync();
|
|
210
|
+
expect(div.classList.contains('inactive')).toBe(false);
|
|
211
|
+
expect(div.classList.contains('active')).toBe(true);
|
|
212
|
+
|
|
213
|
+
button.click();
|
|
214
|
+
flushSync();
|
|
215
|
+
|
|
216
|
+
expect(div.classList.contains('inactive')).toBe(true);
|
|
217
|
+
expect(div.classList.contains('active')).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
145
220
|
it('render dynamic id attribute', () => {
|
|
146
221
|
component Basic() {
|
|
147
222
|
let count = track(0);
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount, flushSync, track } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('head elements', () => {
|
|
5
|
+
let container;
|
|
6
|
+
let originalTitle;
|
|
7
|
+
|
|
8
|
+
function render(component) {
|
|
9
|
+
mount(component, {
|
|
10
|
+
target: container
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
container = document.createElement('div');
|
|
16
|
+
document.body.appendChild(container);
|
|
17
|
+
// Store original title to restore later
|
|
18
|
+
originalTitle = document.title;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
document.body.removeChild(container);
|
|
23
|
+
container = null;
|
|
24
|
+
// Restore original title
|
|
25
|
+
document.title = originalTitle;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders static title element', () => {
|
|
29
|
+
component App() {
|
|
30
|
+
<head>
|
|
31
|
+
<title>{'Static Test Title'}</title>
|
|
32
|
+
</head>
|
|
33
|
+
<div>{'Content'}</div>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(App);
|
|
37
|
+
|
|
38
|
+
expect(document.title).toBe('Static Test Title');
|
|
39
|
+
expect(container.querySelector('div').textContent).toBe('Content');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders reactive title element', () => {
|
|
43
|
+
component App() {
|
|
44
|
+
let title = track('Initial Title');
|
|
45
|
+
|
|
46
|
+
<head>
|
|
47
|
+
<title>{@title}</title>
|
|
48
|
+
</head>
|
|
49
|
+
<div>
|
|
50
|
+
<button onClick={() => { @title = 'Updated Title'; }}>{'Update Title'}</button>
|
|
51
|
+
<span>{@title}</span>
|
|
52
|
+
</div>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(App);
|
|
56
|
+
|
|
57
|
+
expect(document.title).toBe('Initial Title');
|
|
58
|
+
expect(container.querySelector('span').textContent).toBe('Initial Title');
|
|
59
|
+
|
|
60
|
+
const button = container.querySelector('button');
|
|
61
|
+
button.click();
|
|
62
|
+
flushSync();
|
|
63
|
+
|
|
64
|
+
expect(document.title).toBe('Updated Title');
|
|
65
|
+
expect(container.querySelector('span').textContent).toBe('Updated Title');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('renders title with template literal', () => {
|
|
69
|
+
component App() {
|
|
70
|
+
let name = track('World');
|
|
71
|
+
|
|
72
|
+
<head>
|
|
73
|
+
<title>{`Hello ${@name}!`}</title>
|
|
74
|
+
</head>
|
|
75
|
+
<div>
|
|
76
|
+
<button onClick={() => { @name = 'Ripple'; }}>{'Change Name'}</button>
|
|
77
|
+
</div>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render(App);
|
|
81
|
+
|
|
82
|
+
expect(document.title).toBe('Hello World!');
|
|
83
|
+
|
|
84
|
+
const button = container.querySelector('button');
|
|
85
|
+
button.click();
|
|
86
|
+
flushSync();
|
|
87
|
+
|
|
88
|
+
expect(document.title).toBe('Hello Ripple!');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('renders title with computed value', () => {
|
|
92
|
+
component App() {
|
|
93
|
+
let count = track(0);
|
|
94
|
+
let prefix = 'Count: ';
|
|
95
|
+
|
|
96
|
+
<head>
|
|
97
|
+
<title>{prefix + @count}</title>
|
|
98
|
+
</head>
|
|
99
|
+
<div>
|
|
100
|
+
<button onClick={() => { @count++; }}>{'Increment'}</button>
|
|
101
|
+
<span>{@count}</span>
|
|
102
|
+
</div>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
render(App);
|
|
106
|
+
|
|
107
|
+
expect(document.title).toBe('Count: 0');
|
|
108
|
+
|
|
109
|
+
const button = container.querySelector('button');
|
|
110
|
+
button.click();
|
|
111
|
+
flushSync();
|
|
112
|
+
|
|
113
|
+
expect(document.title).toBe('Count: 1');
|
|
114
|
+
expect(container.querySelector('span').textContent).toBe('1');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('handles multiple title updates', () => {
|
|
118
|
+
component App() {
|
|
119
|
+
let step = track(1);
|
|
120
|
+
|
|
121
|
+
<head>
|
|
122
|
+
<title>{`Step ${@step} of 3`}</title>
|
|
123
|
+
</head>
|
|
124
|
+
<div>
|
|
125
|
+
<button onClick={() => { @step = (@step % 3) + 1 }}>{'Next Step'}</button>
|
|
126
|
+
</div>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render(App);
|
|
130
|
+
|
|
131
|
+
expect(document.title).toBe('Step 1 of 3');
|
|
132
|
+
|
|
133
|
+
const button = container.querySelector('button');
|
|
134
|
+
|
|
135
|
+
button.click();
|
|
136
|
+
flushSync();
|
|
137
|
+
expect(document.title).toBe('Step 2 of 3');
|
|
138
|
+
|
|
139
|
+
button.click();
|
|
140
|
+
flushSync();
|
|
141
|
+
expect(document.title).toBe('Step 3 of 3');
|
|
142
|
+
|
|
143
|
+
button.click();
|
|
144
|
+
flushSync();
|
|
145
|
+
expect(document.title).toBe('Step 1 of 3');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('renders empty title', () => {
|
|
149
|
+
component App() {
|
|
150
|
+
<head>
|
|
151
|
+
<title>{''}</title>
|
|
152
|
+
</head>
|
|
153
|
+
<div>{'Empty title test'}</div>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
render(App);
|
|
157
|
+
|
|
158
|
+
expect(document.title).toBe('');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('renders title with conditional content', () => {
|
|
162
|
+
component App() {
|
|
163
|
+
let showPrefix = track(true);
|
|
164
|
+
let title = track('Main Page');
|
|
165
|
+
|
|
166
|
+
<head>
|
|
167
|
+
<title>{@showPrefix ? 'App - ' + @title : @title}</title>
|
|
168
|
+
</head>
|
|
169
|
+
<div>
|
|
170
|
+
<button onClick={() => { @showPrefix = !@showPrefix }}>{'Toggle Prefix'}</button>
|
|
171
|
+
<button onClick={() => { @title = @title === 'Main Page' ? 'Settings' : 'Main Page' }}>{'Change Page'}</button>
|
|
172
|
+
</div>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
render(App);
|
|
176
|
+
|
|
177
|
+
expect(document.title).toBe('App - Main Page');
|
|
178
|
+
|
|
179
|
+
const buttons = container.querySelectorAll('button');
|
|
180
|
+
|
|
181
|
+
// Toggle prefix off
|
|
182
|
+
buttons[0].click();
|
|
183
|
+
flushSync();
|
|
184
|
+
expect(document.title).toBe('Main Page');
|
|
185
|
+
|
|
186
|
+
// Change page
|
|
187
|
+
buttons[1].click();
|
|
188
|
+
flushSync();
|
|
189
|
+
expect(document.title).toBe('Settings');
|
|
190
|
+
|
|
191
|
+
// Toggle prefix back on
|
|
192
|
+
buttons[0].click();
|
|
193
|
+
flushSync();
|
|
194
|
+
expect(document.title).toBe('App - Settings');
|
|
195
|
+
});
|
|
196
|
+
});
|