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>