1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <title>RetroV Test Suite</title>
5 <meta charset="utf-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7 <style>
8 body {
9 background: #000; color: #FFF;
10 font-size: 18px;
11 max-width: 700px;
12 margin: auto;
13 }
14 #test_container { margin-top: 1em; }
15 .result-box {
16 display: inline-block;
17 padding: 4px;
18 border: 1px solid silver;
19 margin: 2px;
20 }
21 .assert-box {
22 display: inline-block;
23 padding: 4px;
24 color: #000;
25 }
26 .assert-box + .assert-box {
27 margin-left: 4px; /* space between assert boxes */
28 }
29 .debug { border: 2px solid blue; }
30 .debug pre { font-size: smaller; background: #27323c; padding: 10px; }
31 .debug-render div { margin: 10px; border: 2px dotted blue; }
32 .debug-render p { margin: 10px; border: 2px dotted green; }
33 .debug-render span { margin: 10px; border: 2px dotted pink; }
34 .good { background: #af5; }
35 .bad { background: red; }
36
37 .tooltip {
38 visibility: hidden;
39 border: 1px solid white;
40 background: black;
41 position: absolute;
42 margin: -30px 20px;
43 padding: 5px;
44 }
45
46 /* click or hover shows tooltip with test description */
47 .result-box:hover .tooltip, .result-box:focus .tooltip {
48 visibility: visible;
49 }
50 .result-box:focus {
51 background: white;
52 }
53
54 .fatal {
55 background: red;
56 margin: 1em;
57 padding: 2em;
58 }
59 </style>
60 </head>
61 <body>
62
63 <h1>RetroV Test Suite</h1>
64
65 <p>If all goes well, you should see a bunch of green boxes and the console will
66 be clear of errors. Roll over (or click on) a test box to see a description.
67 </p>
68
69 <script>
70 /*
71 Welcome to RetroV's simple test suite.
72
73 Unminified/Minified
74 ----------------------------------------------------------
75
76 The first thing to know is that all tests run twice:
77 first against the regular retrov.js source file, then
78 against the minified retrov.min.js file. This ensures
79 that the minified file is up-to-date and that the
80 minification process didn't introduce any new errors.
81
82 Adding Tests
83 ----------------------------------------------------------
84
85 Tests are in the general form of:
86
87 start_test(DESC, VNODES);
88 assert(ASSERTION)
89 assert(ASSERTION)
90 assert(ASSERTION)
91 ...
92 add_render(VNODES);
93 assert(ASSERTION)
94 assert(ASSERTION)
95 assert(ASSERTION)
96 ...
97 end_test();
98
99 DESC = a string description of what the test is doing
100 VNODES = single virtual node or list of virtual nodes
101 ASSERTION = true or falsey statement
102
103 The start_test() function's VNODES will be the
104 intitially-rendered state of a hidden DOM container.
105
106 The hidden DOM container can be updated with calls to
107 add_render().
108
109 Don't forget to call end_test() after a test's
110 assertions to render the output as little green
111 (hopefully) success boxes in the visual results!
112
113 Debugging
114 ----------------------------------------------------------
115
116 Sometimes it's hard to tell why a test is failing
117 because the results are not rendered on the screen.
118
119 Call debug() before any test to have all renders
120 displayed on-screen along with the VNODE data that was
121 passed in for that render.
122
123 debug();
124 start_test(DESC, VNODES);
125 ...
126 end_test();
127
128 Note that debug() turns off after end_test() since it's
129 unlikely you'll want to debug more than one test at a
130 time.
131
132 Good luck!
133 */
134
135
136
137 // The test() function holds all testing code, see where it ends by
138 // finding END_OF_TEST. It is called in another script tag at the end
139 // of this document.
140 function test(output_id, container_id){
141
142 // Hint:
143 // If an assertion about the contents of the container fails,
144 // a fast way to see what was rendered is to put this after
145 // the render:
146 //
147 // output.append(container.innerHTML);
148 //
149
150 var output = document.getElementById(output_id);
151 var container = document.getElementById(container_id);
152
153 var current_test = "not started";
154 var asserts = [];
155
156 var test_count = 0;
157 var assert_count = 0;
158
159 window.onerror = function(message, url, line_num){
160 var fatal_box = document.createElement('div');
161 fatal_box.innerHTML = "Fatal error while running: <b>'"
162 + current_test +
163 "'</b> See console for details.";
164 fatal_box.className = 'fatal';
165 output.append(fatal_box);
166
167 // okay, we're done, false doesn't stop the error from getting
168 // to the default handler (browser)
169 return false;
170 }
171
172 function start_test(description, v){
173 // clear stuff for new test
174 container.replaceChildren();
175 delete container.rv_old_vlist;
176 current_test = description;
177 asserts = [];
178
179 // now render the first v tree
180 RV.render(container, v);
181 if(debug_on){ render_debug(v); }
182 }
183 function add_render(v){
184 // Completely replace whatever was previously in container with
185 // new contents v.
186 RV.render(container, v);
187 if(debug_on){ render_debug(v); }
188 }
189 function end_test(){
190 var box = document.createElement('div');
191 box.className = 'result-box';
192 box.tabIndex = test_count+1;
193
194 var tooltip = document.createElement('div');
195 tooltip.className = 'tooltip';
196 tooltip.innerHTML = current_test;
197 box.append(tooltip);
198
199 // loop over assert results
200 asserts.forEach(function(ar, i){
201 var result = document.createElement('div');
202 result.className = "assert-box " + (ar ? "good" : "bad");
203 result.innerHTML = i+1;
204 box.append(result);
205 });
206
207 output.append(box);
208 debug_on = false; // turn it back off if it was on
209
210 test_count++;
211 }
212 function assert(maybe){
213 asserts.push(maybe);
214 assert_count++;
215
216 if(!maybe){
217 console.error("Failed:",current_test," assert #",assert_count);
218 }
219 }
220
221 function $(query){
222 return container.querySelector(query);
223 }
224
225 // Debugging
226 var debug_on = false;
227
228 function debug(){
229 // Turn debugging on for next test
230 debug_on = true;
231 }
232
233 function render_debug(v){
234 var debug_box = document.createElement('div');
235 debug_box.className = 'debug';
236 debug_box.innerHTML = '<h3>Debug "'+current_test+'" Step:</h3>';
237 var data_box = document.createElement('div');
238 data_box.innerHTML = '<h4>Virtual Nodes ("v"):</h4>';
239 var data_pre = document.createElement('pre');
240 data_pre.innerText = JSON.stringify(v, undefined, 4);
241 data_box.append(data_pre);
242 debug_box.append(data_box);
243 var render_box = document.createElement('div');
244 render_box.innerHTML = '<h4>Actual Nodes (Rendered):</h4>';
245 var container_clone = container.cloneNode(true); // true=deep clone
246 container_clone.id = "";
247 container_clone.className = 'debug-render';
248 render_box.append(container_clone);
249 debug_box.append(render_box);
250
251 output.append(debug_box);
252 }
253
254
255
256
257 /* =======================================================================
258 * The Tests
259 * =======================================================================
260 */
261
262 // See comment way above for general instructions.
263 //
264 // First, tests for rendering in increasing complexity.
265 // Of particular focus are arrays because they're surprisingly challenging:
266 // * Nested arrays are always flattened
267 // * Node children are always completely flattened
268 // (items in arrays in a node child list become just part of the list)
269 // * Arrays that are *not* node children remain arrays
270 // * Sibling elements, in general are tricky because we have to
271 // compare them by sequential position
272 // * Especially deleting them - we have to delete in reverse or the DOM
273 // indexing will not line up (deleting #2 means #3 is now #2!)
274 //
275
276 start_test("Text node", 'foo');
277 assert(container.innerHTML == 'foo')
278 end_test();
279
280 start_test("Empty array should get flattened out", []);
281 assert(container.childNodes.length === 0);
282 end_test();
283
284 start_test("Null should make comment", null);
285 assert(container.innerHTML.includes('RV:null-placeholder'));
286 end_test();
287
288 // The first time we render, undefined values do *not* indicate that
289 // a child or bare array list has changed size. Make placeholder comment.
290 start_test("Undefined should make comment", undefined);
291 assert(container.innerHTML.includes('RV:undefined-placeholder'));
292 end_test();
293
294 start_test("Render a tag", ['div']);
295 assert($('div'));
296 end_test();
297
298 start_test("Render a tag with a text node", ['div', 'foo']);
299 assert($('div'));
300 assert($('div').innerHTML == 'foo');
301 end_test();
302
303 start_test("Render a tag with two text nodes",
304 ['div', 'foo', 'bar']
305 );
306 assert($('div'));
307 assert($('div').innerHTML == 'foobar');
308 end_test();
309
310 start_test("Tag with css class",
311 ['div', {'class':'x'}]
312 );
313 assert($('div.x'));
314 end_test();
315
316 start_test("Tag with css class shorthand",
317 ['span.x']
318 );
319 assert($('span.x'));
320 end_test();
321
322 start_test("Implicit div with css class shorthand",
323 ['.x']
324 );
325 assert($('div.x'));
326 end_test();
327
328 start_test("Implicit div with three css classes shorthand",
329 ['.x.y.z']
330 );
331 assert($('div.x.y.z'));
332 end_test();
333
334 start_test("Implicit div, one shorthand, two regular classes",
335 ['.x', {'class':'y z'}]
336 );
337 assert($('div.x.y.z'));
338 end_test();
339
340 start_test("Render a tag with a data- property and a text node",
341 ['div', {title:'world'}, 'hello']
342 );
343 assert($('[title="world"]'));
344 assert($('div').innerHTML == 'hello');
345 end_test();
346
347 start_test("Two tags in an array",
348 [
349 ['span.a'],
350 ['span.b']
351 ]
352 );
353 assert($('span.a'));
354 assert($('span.b'));
355 end_test();
356
357 start_test("Tag with two child tags",
358 ['div',
359 ['span.a'],
360 ['span.b']
361 ]
362 );
363 assert($('div > span.a'));
364 assert($('div > span.b'));
365 end_test();
366
367 start_test("Tag with array of tags (nested)",
368 ['div',
369 [
370 ['span.a'],
371 ['span.b']
372 ]
373 ]
374 );
375 assert($('div > span.a'));
376 assert($('div > span.b'));
377 end_test();
378
379 start_test("Tag with four children in two symetrical arrays (nested)",
380 ['div',
381 [
382 ['span.a'],
383 ['span.b']
384 ],
385 [
386 ['span.c'],
387 ['span.d']
388 ],
389 ]
390 );
391 assert($('div > span.a'));
392 assert($('div > span.b'));
393 assert($('div > span.c'));
394 assert($('div > span.d'));
395 end_test();
396
397 start_test("Tag with four children in asymetric arrays (nested)",
398 ['div',
399 [
400 ['span.a'],
401 [
402 ['span.b'],
403 ['span.c'],
404 ],
405 ['span.d'],
406 ]
407 ]
408 );
409 assert($('div > span.a'));
410 assert($('div > span.b'));
411 assert($('div > span.c'));
412 assert($('div > span.d'));
413 end_test();
414
415
416 start_test("Tree: 5 children, two grandchildren, weird array nesting",
417 ['div.grandpa',
418 [
419 ['span.a'], // child 1
420 [
421 ['span.b'], // child 2
422 ['span.c'], // child 3
423 ],
424 ['span.d', // child 4
425 [
426 ['span.e', 'f', 'g'],
427 ['span.h', 'i', 'j'],
428 ],
429 ],
430 [
431 [
432 [
433 [
434 ['span.x', 'y', 'z'], // child 5
435 ],
436 ],
437 ],
438 ],
439 ]
440 ]
441 );
442 assert($('div.grandpa > span.a'));
443 assert($('div.grandpa > span.b'));
444 assert($('div.grandpa > span.c'));
445 assert($('div.grandpa > span.d'));
446 assert($('div.grandpa > span.d > span.e'));
447 assert($('div.grandpa > span.d > span.e').innerHTML == 'fg');
448 assert($('div.grandpa > span.d > span.h').innerHTML == 'ij');
449 assert($('div.grandpa > span.x'));
450 assert($('div.grandpa > span.x').innerHTML == 'yz');
451 end_test();
452
453
454 // =======================================================================
455 // Tests for diffing
456
457 before = 'hello';
458 after = 'world';
459 start_test("Simple text change", before);
460 assert(container.innerHTML == 'hello')
461 add_render(after);
462 assert(container.innerHTML == 'world')
463 end_test();
464
465 before = ['div.w',['div.x'],null,['div.z']];
466 after = ['div.w',['div.x'],['div.y'],['div.z']];
467 start_test("Null child placeholder", before);
468 assert($('div.w > div.x'));
469 assert($('div.w > div.z'));
470 add_render(after);
471 assert($('div.w > div.x'));
472 assert($('div.w > div.y'));
473 assert($('div.w > div.z'));
474 end_test();
475
476 before = ['div.w',[['div.x'],null,['div.z']]];
477 after = ['div.w',[['div.x'],['div.y'],['div.z']]];
478 start_test("Null placeholder in array", before);
479 assert($('div.w > div.x'));
480 assert($('div.w > div.z'));
481 add_render(after);
482 assert($('div.w > div.x'));
483 assert($('div.w > div.y'));
484 assert($('div.w > div.z'));
485 end_test();
486
487 before = ['div.w',['div.x'],undefined,['div.z']];
488 after = ['div.w',['div.x'],['div.y'],['div.z']];
489 start_test("Undefined child placeholder", before);
490 assert($('div.w > div.x'));
491 assert($('div.w > div.z'));
492 add_render(after);
493 assert($('div.w > div.x'));
494 assert($('div.w > div.y'));
495 assert($('div.w > div.z'));
496 end_test();
497
498 before = ['div.w',[['div.x'],undefined,['div.z']]];
499 after = ['div.w',[['div.x'],['div.y'],['div.z']]];
500 start_test("Undefined placeholder in array", before);
501 assert($('div.w > div.x'));
502 assert($('div.w > div.z'));
503 add_render(after);
504 assert($('div.w > div.x'));
505 assert($('div.w > div.y'));
506 assert($('div.w > div.z'));
507 end_test();
508
509 before = ['div.w',['div.x'],[],['div.z']];
510 after = ['div.w',['div.x'],['div.y'],['div.z']];
511 start_test("Div replaces empty array", before);
512 assert($('div.w > div.x'));
513 assert($('div.w > div.z'));
514 add_render(after);
515 assert($('div.w > div.x'));
516 assert($('div.w > div.y'));
517 assert($('div.w > div.z'));
518 end_test();
519
520 before = [];
521 after = [['div.x'],['div.y'],['div.z']];
522 start_test("Multiple array items added, then removed", before);
523 // not much to assert here, container starts empty
524 add_render(after);
525 assert($('div.x'));
526 assert($('div.y'));
527 assert($('div.z'));
528 add_render(before);
529 assert(!$('div.x')); // not
530 assert(!$('div.y')); // not
531 assert(!$('div.z')); // not
532 end_test();
533
534 before = ['div.w'];
535 after = ['div.w',['div.x'],['div.y'],['div.z']];
536 start_test("Multiple children added then removed", before);
537 assert($('div.w'));
538 add_render(after);
539 assert($('div.w > div.x'));
540 assert($('div.w > div.y'));
541 assert($('div.w > div.z'));
542 add_render(before);
543 assert(!$('div.w > div.x')); // not
544 assert(!$('div.w > div.y')); // not
545 assert(!$('div.w > div.z')); // not
546 end_test();
547
548 before = ['div.w',['div.x']];
549 after = ['div.w',['div.x'],['div.y'],['div.z']];
550 start_test("Multiple children added (to existing) then removed", before);
551 assert($('div.w > div.x'));
552 add_render(after);
553 assert($('div.w > div.x'));
554 assert($('div.w > div.y'));
555 assert($('div.w > div.z'));
556 add_render(before);
557 assert($('div.w > div.x'));
558 assert(!$('div.w > div.y')); // not
559 assert(!$('div.w > div.z')); // not
560 end_test();
561
562 before = ['div.w', []];
563 after = ['div.w', [['div.x'],['div.y'],['div.z']]];
564 start_test("Multiple child array items added, then removed", before);
565 assert($('div.w'));
566 add_render(after);
567 assert($('div.w > div.x'));
568 assert($('div.w > div.y'));
569 assert($('div.w > div.z'));
570 add_render(before);
571 assert(!$('div.w > div.x')); // not
572 assert(!$('div.w > div.y')); // not
573 assert(!$('div.w > div.z')); // not
574 end_test();
575
576 before = ['div.w',['div.x'],['div.y'],['div.z']];
577 after = ['div.w',['div.x'],[],['div.z']];
578 start_test("Empty array replaces div and back again", before);
579 assert($('div.w > div.x'));
580 assert($('div.w > div.y'));
581 assert($('div.w > div.z'));
582 add_render(after);
583 assert($('div.w > div.x'));
584 assert(!$('div.w > div.y')); // not
585 assert($('div.w > div.z'));
586 add_render(before);
587 assert($('div.w > div.x'));
588 assert($('div.w > div.y'));
589 assert($('div.w > div.z'));
590 end_test();
591
592 before = ['div.w',['div.x'],[],['div.z']];
593 after = ['div.w',['div.x'],[['div.a'],['div.b']],['div.z']];
594 start_test("Empty array filled, then emptied again.", before);
595 assert($('div.w > div.x'));
596 assert($('div.w > div.z'));
597 add_render(after);
598 assert($('div.w > div.x'));
599 assert($('div.w > div.a'));
600 assert($('div.w > div.b'));
601 assert($('div.w > div.z'));
602 add_render(before);
603 assert($('div.w > div.x'));
604 assert(!$('div.w > div.a')); // not
605 assert(!$('div.w > div.b')); // not
606 assert($('div.w > div.z'));
607 end_test();
608
609 //debug();
610 before = ['div.hello', ['p.a', 'a'],['p.b', 'b'],['p.c', 'c'],['p.d', 'd'],['p.e', 'e']];
611 after = ['div.hello', ['p.a', 'a'],['p.b', 'b'],['p.c', 'c']];
612 start_test("List shortened, should remove items at end.", before);
613 assert($('div.hello > p.a'));
614 assert($('div.hello > p.b'));
615 assert($('div.hello > p.c'));
616 assert($('div.hello > p.d'));
617 assert($('div.hello > p.e'));
618 add_render(after);
619 assert($('div.hello > p.a'));
620 assert($('div.hello > p.b'));
621 assert($('div.hello > p.c'));
622 assert(!$('div.hello > p.d')); // not
623 assert(!$('div.hello > p.e')); // not
624 end_test();
625
626 before= ['div.a', 'A'];
627 after = ['div.b', 'B'];
628 start_test("Change class and text child, and back again", before);
629 assert($('div.a').innerHTML == 'A');
630 add_render(after);
631 assert(!$('div.a')); // not
632 assert($('div.b').innerHTML == 'B');
633 add_render(before);
634 assert(!$('div.b')); // not
635 assert($('div.a').innerHTML == 'A');
636 end_test();
637
638 before= ['div.a', 'A'];
639 after = ['span.b', 'B'];
640 start_test("Change tag, class, and text child, and back again", before);
641 assert($('div.a').innerHTML == 'A');
642 add_render(after);
643 assert(!$('div.a')); // not
644 assert($('span.b').innerHTML == 'B');
645 add_render(before);
646 assert(!$('span.b')); // not
647 assert($('div.a').innerHTML == 'A');
648 end_test();
649
650 before = ['form',
651 ['p', 'Make a character!'],
652 ['label', 'Character Name:',
653 ['input', {type:'text',placeholder:'Your Name Here'}]
654 ],
655 ['p', 'Are you a goose?'],
656 ['label', 'Yes',
657 ['input', {type:'radio',name:'honk',value:'yes'}]
658 ],
659 ['label', 'No',
660 ['input', {type:'radio',name:'honk',value:'no'}]
661 ],
662 ['button', 'Create Character'],
663 ];
664 start_test("Completely change, empty, change, back again.", before);
665 assert($('form'));
666 assert($('form').childNodes.length == 6);
667 assert($('form > p'));
668 assert($('form > label'));
669 assert($('form > label > input[type="text"'));
670 assert($('form > label > input[value="yes"]'));
671 assert($('form > label > input[value="no"]'));
672 assert($('form > button').innerHTML == 'Create Character');
673 add_render([]);
674 assert(!$('form')); // not
675 add_render(['.nothing', 'nothing', [], [], null, []]);
676 assert($('.nothing'));
677 assert($('.nothing').innerHTML.includes('RV:null-placeholder'));
678 add_render(['.something', ['span','hi'], [], ['span','bye'], []]);
679 assert($('.something'));
680 assert($('.something > span'));
681 add_render(null);
682 assert(container.innerHTML.includes('RV:null-placeholder'));
683 add_render(['form', 'Write an essay:', ['textarea']]);
684 assert($('form > textarea'));
685 add_render(before);
686 // exact repeat of the assertions above
687 assert($('form'));
688 assert($('form').childNodes.length == 6);
689 assert($('form > p'));
690 assert($('form > label'));
691 assert($('form > label > input[type="text"'));
692 assert($('form > label > input[value="yes"]'));
693 assert($('form > label > input[value="no"]'));
694 assert($('form > button').innerHTML == 'Create Character');
695 end_test();
696
697
698 // =======================================================================
699 // Tests with "false means 'no change'"
700
701 before= ['div.a', 'A'];
702 after = false;
703 start_test("No render with false, then re-render", before);
704 assert($('div.a').innerHTML == 'A');
705 add_render(after);
706 assert($('div.a').innerHTML == 'A');
707 add_render(before);
708 assert($('div.a').innerHTML == 'A');
709 end_test();
710
711 before= ['div.a', ['.cb'], ['.cc']];
712 after = ['div.a', false, false];
713 start_test("No render with false (children) twice, then re-render", before);
714 assert($('div.a > div.cb'));
715 assert($('div.a > div.cc'));
716 add_render(after);
717 assert($('div.a > div.cb'));
718 assert($('div.a > div.cc'));
719 add_render(after);
720 assert($('div.a > div.cb'));
721 assert($('div.a > div.cc'));
722 add_render(before);
723 assert($('div.a > div.cb'));
724 assert($('div.a > div.cc'));
725 end_test();
726
727 before= ['div.a', ['.cb'], ['.cc']];
728 after = ['div.a', false, false];
729 after2 = ['div.a', ['span.cb'], ['span.cc']];
730 start_test("No render with false (children), then different", before);
731 assert($('div.a > div.cb'));
732 assert($('div.a > div.cc'));
733 add_render(after);
734 assert($('div.a > div.cb'));
735 assert($('div.a > div.cc'));
736 add_render(after2);
737 assert($('div.a > span.cb'));
738 assert($('div.a > span.cc'));
739 add_render(before);
740 assert($('div.a > div.cb'));
741 assert($('div.a > div.cc'));
742 end_test();
743
744 before = [['.a'], ['.b'], ['.c']];
745 after = [false, ['.b'], false];
746 after2 = [false, false, false];
747 start_test("No render with false (array)", before);
748 assert($('div.a'));
749 assert($('div.b'));
750 assert($('div.c'));
751 add_render(after);
752 assert($('div.a'));
753 assert($('div.b'));
754 assert($('div.c'));
755 add_render(after2);
756 assert($('div.a'));
757 assert($('div.b'));
758 assert($('div.c'));
759 add_render(before);
760 assert($('div.a'));
761 assert($('div.b'));
762 assert($('div.c'));
763 end_test();
764
765 before = ['.foo'];
766 start_test("False, null, arrays, elements switch-a-roo", before);
767 assert($('div.foo'));
768 add_render(false);
769 assert($('div.foo'));
770 add_render(null);
771 assert(container.innerHTML.includes('RV:null-placeholder'));
772 add_render(false);
773 assert(container.innerHTML.includes('RV:null-placeholder'));
774 add_render(before);
775 assert($('div.foo'));
776 add_render(false);
777 add_render([]);
778 add_render(null);
779 add_render(false);
780 add_render([]);
781 add_render(false); // just seeing if it will blow up
782 add_render(before);
783 assert($('div.foo'));
784 end_test();
785
786 before = false;
787 start_test("False placeholder", before);
788 assert(container.innerHTML.includes('RV:false-placeholder'));
789 add_render(['.foo']);
790 assert($('div.foo'));
791 end_test();
792
793 before = ['.foo', false];
794 after = ['.foo', ['.bar']];
795 start_test("False placeholder child", before);
796 assert($('.foo').innerHTML.includes('RV:false-placeholder'));
797 add_render(after);
798 assert($('div.foo > .bar'));
799 add_render(before);
800 assert($('div.foo > .bar'));
801 end_test();
802
803 before = ['.foo', ['.bar', false, ['.baz']]];
804 after = ['.foo', ['.bar', ['.bonk'], ['.baz']]];
805 after2 = ['.foo', false];
806 after3 = false;
807 start_test("False placeholder grandchild", before);
808 assert($('div.foo > .bar > .baz'));
809 assert($('.foo > .bar').innerHTML.includes('RV:false-placeholder'));
810 add_render(after);
811 assert($('div.foo > .bar'));
812 assert($('div.foo > .bar > .baz'));
813 assert($('div.foo > .bar > .bonk'));
814 add_render(after2);
815 assert($('div.foo > .bar'));
816 assert($('div.foo > .bar > .baz'));
817 assert($('div.foo > .bar > .bonk'));
818 add_render(after3);
819 assert($('div.foo > .bar'));
820 assert($('div.foo > .bar > .baz'));
821 assert($('div.foo > .bar > .bonk'));
822 end_test();
823
824 // =======================================================================
825 // Styles
826
827 before = ['div', {
828 style: {
829 color: 'red',
830 fontSize: '2em',
831 },
832 },
833 ['span', { // child of div
834 style: {
835 backgroundColor: 'green',
836 }
837 }],
838 ];
839 after = ['div', {
840 style: {
841 color: 'red',
842 fontSize: '3em',
843 },
844 },
845 ['span', { // child of div
846 style: {
847 textDecoration: 'underline',
848 }
849 }],
850 ];
851 start_test("Set and change styles", before);
852 assert($('div').style.color === 'red');
853 assert($('div').style.fontSize === '2em');
854 assert($('div > span').style.backgroundColor === 'green');
855 add_render(after);
856 assert($('div').style.color === 'red');
857 assert($('div').style.fontSize === '3em');
858 assert($('div > span').style.backgroundColor === 'green');
859 assert($('div > span').style.textDecoration === 'underline');
860 add_render(before);
861 assert($('div').style.color === 'red');
862 assert($('div').style.fontSize === '2em');
863 assert($('div > span').style.backgroundColor === 'green');
864 end_test();
865
866 // =======================================================================
867 // Misc
868
869 // Special handling of label "for" attribute (is htmlFor in JS!)
870 start_test("Set label 'for' and change it", ['label', {'for':'X'}]);
871 assert($('label').htmlFor === 'X');
872 add_render(['label', {htmlFor:'Y'}]); // via real js name
873 assert($('label').htmlFor === 'Y');
874 add_render(['label', {'for':'Z'}]);
875 assert($('label').htmlFor === 'Z');
876 end_test();
877
878 // Special handling of form elements since their values can change
879 // on their own!
880 start_test("Set value for input, change it, set it again",
881 ['input', {type:'text', value:'apple'}]);
882 assert($('input').value === 'apple');
883 $('input').value = 'grape';
884 assert($('input').value === 'grape');
885 // IMPORTANT: this is the SAME value we had in prev render:
886 add_render(['input', {type:'text', value:'apple'}]);
887 assert($('input').value === 'apple');
888 add_render(['input', {type:'text', value:'pear'}]);
889 assert($('input').value === 'pear');
890 end_test();
891
892 start_test("Set value for textarea, change it, set it again",
893 ['textarea', {type:'text', value:'apple'}]);
894 assert($('textarea').value === 'apple');
895 $('textarea').value = 'grape';
896 assert($('textarea').value === 'grape');
897 // IMPORTANT: this is the SAME value we had in prev render:
898 add_render(['textarea', {type:'text', value:'apple'}]);
899 assert($('textarea').value === 'apple');
900 add_render(['textarea', {type:'text', value:'pear'}]);
901 assert($('textarea').value === 'pear');
902 end_test();
903
904 start_test("Set checkbox checked, change it, set again",
905 ['input', {type:'checkbox', checked:true}]);
906 assert($('input[type="checkbox"]').checked);
907 $('input[type="checkbox"]').checked = false;
908 assert(!$('input[type="checkbox"]').checked); // not
909 // IMPORTANT: same as original render
910 add_render(['input', {type:'checkbox', checked:true}]);
911 assert($('input[type="checkbox"]').checked);
912 add_render(['input', {type:'checkbox', checked:false}]);
913 assert(!$('input[type="checkbox"]').checked); // not
914 end_test();
915
916 var my_input_ref = null;
917 start_test("Use oncreate to store ref, manipulate element",
918 ['input', {
919 value: 'TEST1',
920 oncreate:function(el){
921 my_input_ref = el;
922 },
923 }]
924 );
925 assert($('input').value === 'TEST1');
926 my_input_ref.value = 'TEST2'; // manually change it through reference
927 assert($('input').value === 'TEST2');
928 end_test();
929
930 var oncreate_call_count = 0;
931 start_test("Use oncreate is called only for current",
932 ['div', {
933 oncreate:function(el){
934 oncreate_call_count += 1;
935 },
936 }]
937 );
938 add_render(['div']);
939 assert(oncreate_call_count === 1);
940 end_test();
941
942 var my_element_reference = null;
943 start_test("Use rvid to store ref, compare with oncreate",
944 ['input', {
945 rvid: 'RVID1',
946 oncreate:function(el){
947 my_element_reference = el;
948 },
949 }]
950 );
951 assert(my_element_reference === RV.id.RVID1);
952 end_test();
953
954 start_test("Raw (verbatim) HTML", ['div']);
955 assert($('div'));
956 add_render(['div', ['<span class="hi">Hello</span>']]);
957 assert($('div > span.hi'));
958 assert($('div > span.hi').innerHTML == 'Hello');
959 add_render(['div', ['<span class="hi">Bye</span>']]);
960 // 2025-08-17: raw HTML now re-evaluates!
961 assert($('div > span.hi').innerHTML == 'Bye');
962 add_render(['div', ['b', 'B'], ['<span class="hi">Okay</span>']]);
963 assert($('div > b'));
964 assert($('div > span.hi'));
965 assert($('div > span.hi').innerHTML == 'Okay');
966 add_render(['div']);
967 assert(!$('div > span.hi')); // not
968 end_test();
969
970
971
972 // =======================================================================
973 // Regression tests (testing fixed bugs)
974
975 start_test("Non-empty DOM container should be emptied on first run", ['h1', 'Hello'] );
976 // This one is different. The above start_test() will insert some stuff
977 // and RetroV will set the "old_v" (rv_old_vlist) on the container. We're
978 // going to manually remove the rv_old_vlist to simulate having a non-empty
979 // container that appears to have never been rendered into before (as one
980 // might encounter when rendering to document.body for the first time).
981 assert($('h1').innerHTML == 'Hello') // has stuff
982 delete container.rv_old_vlist; // will appear to never have been rendered into before
983 add_render(['h2', ['i', 'Hello again']]);
984 assert(!$('h1')); // should not exist anymore, render thinks its first time
985 assert($('h2 i').innerHTML == 'Hello again');
986 // test rendering a second time - if the DOM wasn't cleared correctly before,
987 // we will CRASH because the DOM child numbering will be off for the update!
988 add_render(['h2', ['i', 'Hello yet again']]);
989 assert($('h2 i').innerHTML == 'Hello yet again');
990 end_test();
991
992 start_test("Null in second position is NOT a properties object, LOL",
993 ['h1', null, 'Did not crash']);
994 // This test will flat-out crash with the initial bug present. But the assert
995 // makes certain that we did, in fact, render correctly (after not crashing).
996 assert($('h1').innerHTML.includes('Did not crash'));
997 end_test();
998
999 start_test("Updating null with same should not attempt to update properties",
1000 ['h1', null, null]);
1001 assert($('h1'));
1002 add_render(['h1', null, null]);
1003 assert($('h1')); // just confirms we didn't crash :-)
1004 end_test();
1005
1006 start_test("Updating undefined with same should not attempt to update properties",
1007 ['h1', undefined, undefined]);
1008 assert($('h1'));
1009 add_render(['h1', undefined, undefined]);
1010 assert($('h1')); // just confirms we didn't crash :-)
1011 end_test();
1012
1013 start_test("Replacing tag with list should replace, not 'append'",
1014 ['h2.info', 'start']);
1015 add_render(['div', 'foo']);
1016 add_render([ ['h2', 'bar'] ]);
1017 assert($('h2').innerHTML == 'bar'); // should exist
1018 assert(!$('div')); // original tag must NOT exist
1019 end_test();
1020
1021 start_test("Ending list with empty list '[]' as last item should not clear whole list.",
1022 [['p.foo', 'Foo'], ['p.bar', 'Bar'], []]);
1023 assert($('p.foo')); // First tag should exist...
1024 assert($('p.bar')); // Second tag should exist, etc.
1025 end_test();
1026
1027 // =======================================================================
1028 // All done, print simple count summary
1029
1030 container.replaceChildren(
1031 '' + test_count + ' tests and ' + assert_count + ' assertions done!'
1032 );
1033
1034 } // END_OF_TEST
1035
1036 </script>
1037
1038 <h2>Unminified:</h2>
1039 <div id="test_output"></div>
1040 <div id="test_container"></div>
1041 <script src="retrov.js"></script>
1042 <script>test('test_output', 'test_container');</script>
1043
1044 <h2>Minified:</h2>
1045 <div id="test_output_min"></div>
1046 <div id="test_container_min"></div>
1047 <script src="retrov.min.js"></script>
1048 <script>test('test_output_min', 'test_container_min');</script>
1049
1050 <br><br>
1051
1052 </body>
1053 </html>