initial import
[vuplus_webkit] / Tools / CSSTestSuiteHarness / harness / harness.js
1 /*
2  * Copyright (C) 2010 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 // requires jQuery
27
28 const kTestSuiteVersion = '20101001';
29 const kTestSuiteHome = '../' + kTestSuiteVersion + '/';
30 const kTestInfoDataFile = 'testinfo.data';
31
32 const kChapterData = [
33   {
34     'file' : 'about.html',
35     'title' : 'About the CSS 2.1 Specification',
36   },
37   {
38     'file' : 'intro.html',
39     'title' : 'Introduction to CSS 2.1',
40   },
41   {
42     'file' : 'conform.html',
43     'title' : 'Conformance: Requirements and Recommendations',
44   },
45   {
46     'file' : "syndata.html",
47     'title' : 'Syntax and basic data types',
48   },
49   {
50     'file' : 'selector.html' ,
51     'title' : 'Selectors',
52   },
53   {
54     'file' : 'cascade.html',
55     'title' : 'Assigning property values, Cascading, and Inheritance',
56   },
57   {
58     'file' : 'media.html',
59     'title' : 'Media types',
60   },
61   {
62     'file' : 'box.html' ,
63     'title' : 'Box model',
64   },
65   {
66     'file' : 'visuren.html',
67     'title' : 'Visual formatting model',
68   },
69   {
70     'file' :'visudet.html',
71     'title' : 'Visual formatting model details',
72   },
73   {
74     'file' : 'visufx.html',
75     'title' : 'Visual effects',
76   },
77   {
78     'file' : 'generate.html',
79     'title' : 'Generated content, automatic numbering, and lists',
80   },
81   {
82     'file' : 'page.html',
83     'title' : 'Paged media',
84   },
85   {
86     'file' : 'colors.html',
87     'title' : 'Colors and Backgrounds',
88   },
89   {
90     'file' : 'fonts.html',
91     'title' : 'Fonts',
92   },
93   {
94     'file' : 'text.html',
95     'title' : 'Text',
96   },
97   {
98     'file' : 'tables.html',
99     'title' : 'Tables',
100   },
101   {
102     'file' : 'ui.html',
103     'title' : 'User interface',
104   },
105   {
106     'file' : 'aural.html',
107     'title' : 'Appendix A. Aural style sheets',
108   },
109   {
110     'file' : 'refs.html',
111     'title' : 'Appendix B. Bibliography',
112   },
113   {
114     'file' : 'changes.html',
115     'title' : 'Appendix C. Changes',
116   },
117   {
118     'file' : 'sample.html',
119     'title' : 'Appendix D. Default style sheet for HTML 4',
120   },
121   {
122     'file' : 'zindex.html',
123     'title' : 'Appendix E. Elaborate description of Stacking Contexts',
124   },
125   {
126     'file' : 'propidx.html',
127     'title' : 'Appendix F. Full property table',
128   },
129   {
130     'file' : 'grammar.html',
131     'title' : 'Appendix G. Grammar of CSS',
132   },
133   {
134     'file' : 'other.html',
135     'title' : 'Other',
136   },
137 ];
138
139
140 const kHTML4Data = {
141   'path' : 'html4',
142   'suffix' : '.htm'
143 };
144
145 const kXHTML1Data = {
146   'path' : 'xhtml1',
147   'suffix' : '.xht'
148 };
149
150 // Results popup
151 const kResultsSelector = [
152   {
153     'name': 'All Tests',
154     'handler' : function(self) { self.showResultsForAllTests(); },
155     'exporter' : function(self) { self.exportResultsForAllTests(); }
156   },
157   {
158     'name': 'Completed Tests',
159     'handler' : function(self) { self.showResultsForCompletedTests(); },
160     'exporter' : function(self) { self.exportResultsForCompletedTests(); }
161   },
162   {
163     'name': 'Passing Tests',
164     'handler' : function(self) { self.showResultsForTestsWithStatus('pass'); },
165     'exporter' : function(self) { self.exportResultsForTestsWithStatus('pass'); }
166   },
167   {
168     'name': 'Failing Tests',
169     'handler' : function(self) { self.showResultsForTestsWithStatus('fail'); },
170     'exporter' : function(self) { self.exportResultsForTestsWithStatus('fail'); }
171   },
172   {
173     'name': 'Skipped Tests',
174     'handler' : function(self) { self.showResultsForTestsWithStatus('skipped'); },
175     'exporter' : function(self) { self.exportResultsForTestsWithStatus('skipped'); }
176   },
177   {
178     'name': 'Invalid Tests',
179     'handler' : function(self) { self.showResultsForTestsWithStatus('invalid'); },
180     'exporter' : function(self) { self.exportResultsForTestsWithStatus('invalid'); }
181   },
182   {
183     'name': 'Tests where HTML4 and XHTML1 results differ',
184     'handler' : function(self) { self.showResultsForTestsWithMismatchedResults(); },
185     'exporter' : function(self) { self.exportResultsForTestsWithMismatchedResults(); }
186   },
187   {
188     'name': 'Tests Not Run',
189     'handler' : function(self) { self.showResultsForTestsNotRun(); },
190     'exporter' : function(self) { self.exportResultsForTestsNotRun(); }
191   }
192 ];
193
194 function Test(testInfoLine)
195 {
196   var fields = testInfoLine.split('\t');
197   
198   this.id = fields[0];
199   this.reference = fields[1];
200   this.title = fields[2];
201   this.flags = fields[3];
202   this.links = fields[4];
203   this.assertion = fields[5];
204
205   this.paged = false;
206   this.testHTML = true;
207   this.testXHTML = true;
208
209   if (this.flags) {
210     this.paged = this.flags.indexOf('paged') != -1;
211   
212     if (this.flags.indexOf('nonHTML') != -1)
213       this.testHTML = false;
214
215     if (this.flags.indexOf('HTMLonly') != -1)
216       this.testXHTML = false;
217   }
218   
219   this.completedHTML = false; // true if this test has a result (pass, fail or skip)
220   this.completedXHTML = false; // true if this test has a result (pass, fail or skip)
221
222   this.statusHTML = '';
223   this.statusXHTML = '';
224   
225   if (!this.links)
226     this.links = "other.html"
227 }
228
229 Test.prototype.runForFormat = function(format)
230 {
231   if (format == 'html4')
232     return this.testHTML;
233
234   if (format == 'xhtml1')
235     return this.testXHTML;
236
237   return true;
238 }
239
240 Test.prototype.completedForFormat = function(format)
241 {
242   if (format == 'html4')
243     return this.completedHTML;
244
245   if (format == 'xhtml1')
246     return this.completedXHTML;
247
248   return true;
249 }
250
251 Test.prototype.statusForFormat = function(format)
252 {
253   if (format == 'html4')
254     return this.statusHTML;
255
256   if (format == 'xhtml1')
257     return this.statusXHTML;
258
259   return true;
260 }
261
262 function ChapterSection(link)
263 {
264   var result= link.match(/^([.\w]+)(#.+)?$/);
265   if (result != null) {
266     this.file = result[1];
267     this.anchor = result[2];
268   }
269
270   this.testCountHTML = 0;
271   this.testCountXHTML = 0;
272
273   this.tests = [];
274 }
275
276 ChapterSection.prototype.countTests = function()
277 {
278   this.testCountHTML = 0;
279   this.testCountXHTML = 0;
280
281   for (var i = 0; i < this.tests.length; ++i) {
282     var currTest = this.tests[i];
283
284     if (currTest.testHTML)
285       ++this.testCountHTML;
286
287     if (currTest.testXHTML)
288       ++this.testCountXHTML;
289   }
290 }
291
292 function Chapter(chapterInfo)
293 {
294   this.file = chapterInfo.file;
295   this.title = chapterInfo.title;
296   this.testCountHTML = 0;
297   this.testCountXHTML = 0;
298   this.sections = []; // array of ChapterSection
299 }
300
301 Chapter.prototype.description = function(format)
302 {
303   
304   
305   return this.title + ' (' + this.testCount(format) + ' tests, ' + this.untestedCount(format) + ' untested)';
306 }
307
308 Chapter.prototype.countTests = function()
309 {
310   this.testCountHTML = 0;
311   this.testCountXHTML = 0;
312
313   for (var i = 0; i < this.sections.length; ++i) {
314     var currSection = this.sections[i];
315
316     currSection.countTests();
317
318     this.testCountHTML += currSection.testCountHTML;
319     this.testCountXHTML += currSection.testCountXHTML;
320   }
321 }
322
323 Chapter.prototype.testCount = function(format)
324 {
325   if (format == 'html4')
326     return this.testCountHTML;
327
328   if (format == 'xhtml1')
329     return this.testCountXHTML;
330
331   return 0;
332 }
333
334 Chapter.prototype.untestedCount = function(format)
335 {
336   var completedProperty = format == 'html4' ? 'completedHTML' : 'completedXHTML';
337   
338   var count = 0;
339   for (var i = 0; i < this.sections.length; ++i) {
340     var currSection = this.sections[i];
341     for (var j = 0; j < currSection.tests.length; ++j) {
342       count += currSection.tests[j].completedForFormat(format) ? 0 : 1;
343     }
344   }
345   return count;
346   
347 }
348
349 // Utils
350 String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }
351
352 function TestSuite()
353 {
354   this.chapterSections = {}; // map of links to ChapterSections
355   this.tests = {}; // map of test id to test info
356   
357   this.chapters = {}; // map of file name to chapter
358   this.currentChapter = null;
359   
360   this.currentChapterTests = []; // array of tests for the current chapter.
361   this.currChapterTestIndex = -1; // index of test in the current chapter
362   
363   this.format = '';
364   this.formatChanged('html4');
365   
366   this.testInfoLoaded = false;
367
368   this.populatingDatabase = false;
369   
370   var testInfoPath = kTestSuiteHome + kTestInfoDataFile;
371   this.loadTestInfo(testInfoPath);
372 }
373
374 TestSuite.prototype.loadTestInfo = function(testInfoPath)
375 {
376   var _self = this;
377   this.asyncLoad(testInfoPath, 'data', function(data, status) {
378     _self.testInfoDataLoaded(data, status);
379   });
380 }
381
382 TestSuite.prototype.testInfoDataLoaded = function(data, status)
383 {
384   if (status != 'success') {
385     alert("Failed to load testinfo.data. Database of tests will not be initialized.");
386     return;
387   }
388
389   this.parseTests(data);
390   this.buildChapters();
391
392   this.testInfoLoaded = true;
393   
394   this.fillChapterPopup();
395
396   this.initializeControls();
397
398   this.openDatabase();
399 }
400
401 TestSuite.prototype.parseTests = function(data)
402 {
403   var lines = data.split('\n');
404   
405   // First line is column labels
406   for (var i = 1; i < lines.length; ++i) {
407     var test = new Test(lines[i]);
408     if (test.id.length > 0)
409       this.tests[test.id] = test;
410   }
411 }
412
413 TestSuite.prototype.buildChapters = function()
414 {
415   for (var testID in this.tests) {
416     var currTest = this.tests[testID];
417
418     // FIXME: tests with more than one link will be presented to the user
419     // twice. Be smarter about avoiding this.
420     var testLinks = currTest.links.split(',');
421     for (var i = 0; i < testLinks.length; ++i) {
422       var link = testLinks[i];
423       var section = this.chapterSections[link];
424       if (!section) {
425         section = new ChapterSection(link);
426         this.chapterSections[link] = section;
427       }
428       
429       section.tests.push(currTest);
430     }
431   }
432   
433   for (var i = 0; i < kChapterData.length; ++i) {
434     var chapter = new Chapter(kChapterData[i]);
435     chapter.index = i;
436     this.chapters[chapter.file] = chapter;
437   }
438   
439   for (var sectionName in this.chapterSections) {
440     var section = this.chapterSections[sectionName];
441
442     var file = section.file;
443     var chapter = this.chapters[file];
444     if (!chapter)
445       window.console.log('failed to find chapter ' + file + ' in chapter data.');
446     chapter.sections.push(section);
447   }
448   
449   for (var chapterName in this.chapters) {
450     var currChapter = this.chapters[chapterName];
451     currChapter.sections.sort();
452     currChapter.countTests();
453   }
454 }
455
456 TestSuite.prototype.indexOfChapter = function(chapter)
457 {
458   for (var i = 0; i < kChapterData.length; ++i) {
459     if (kChapterData[i].file == chapter.file)
460       return i;
461   }
462   
463   window.console.log('indexOfChapter for ' + chapter.file + ' failed');
464   return -1;
465 }
466
467 TestSuite.prototype.chapterAtIndex = function(index)
468 {
469   if (index < 0 || index >= kChapterData.length)
470     return null;
471
472   return this.chapters[kChapterData[index].file];
473 }
474
475 TestSuite.prototype.fillChapterPopup = function()
476 {
477   var select = document.getElementById('chapters')
478   select.innerHTML = ''; // Remove all children.
479   
480   for (var i = 0; i < kChapterData.length; ++i) {
481     var chapterData = kChapterData[i];
482     var chapter = this.chapters[chapterData.file];
483     
484     var option = document.createElement('option');
485     option.innerText = chapter.description(this.format);
486     option._chapter = chapter;
487     
488     select.appendChild(option);
489   }
490 }
491
492 TestSuite.prototype.updateChapterPopup = function()
493 {
494   var select = document.getElementById('chapters')
495   var currOption = select.firstChild;
496   
497   for (var i = 0; i < kChapterData.length; ++i) {
498     var chapterData = kChapterData[i];
499     var chapter = this.chapters[chapterData.file];
500     if (!chapter)
501       continue;
502     currOption.innerText = chapter.description(this.format);
503     currOption = currOption.nextSibling;
504   }
505 }
506
507 TestSuite.prototype.buildTestListForChapter = function(chapter)
508 {
509   this.currentChapterTests = this.testListForChapter(chapter);
510 }
511
512 TestSuite.prototype.testListForChapter = function(chapter)
513 {
514   var testList = [];
515   
516   for (var i in chapter.sections) {
517     var currSection = chapter.sections[i];
518     
519     for (var j = 0; j < currSection.tests.length; ++j) {
520       var currTest = currSection.tests[j];
521       if (currTest.runForFormat(this.format))
522         testList.push(currTest);
523     }
524   }
525   
526   // FIXME: test may occur more than once.
527   testList.sort(function(a, b) {
528     return a.id.localeCompare(b.id);
529   });
530   
531   return testList;
532 }
533
534 TestSuite.prototype.initializeControls = function()
535 {
536   var chaptersPopup = document.getElementById('chapters');
537
538   var _self = this;
539   chaptersPopup.addEventListener('change', function() {
540     _self.chapterPopupChanged();
541   }, false);
542
543   this.chapterPopupChanged();
544   
545   // Results popup
546   var resultsPopup = document.getElementById('results-popup');
547   resultsPopup.innerHTML = '';
548   
549   for (var i = 0; i < kResultsSelector.length; ++i) {
550     var option = document.createElement('option');
551     option.innerText =  kResultsSelector[i].name;
552     
553     resultsPopup.appendChild(option);
554   }
555 }
556
557 TestSuite.prototype.chapterPopupChanged = function()
558 {
559   var chaptersPopup = document.getElementById('chapters');
560   var selectedChapter = chaptersPopup.options[chaptersPopup.selectedIndex]._chapter;
561
562   this.setSelectedChapter(selectedChapter);
563 }
564
565 TestSuite.prototype.fillTestList = function()
566 {
567   var statusProperty = this.format == 'html4' ? 'statusHTML' : 'statusXHTML';
568
569   var testList = document.getElementById('test-list');
570   testList.innerHTML = '';
571   
572   for (var i = 0; i < this.currentChapterTests.length; ++i) {
573     var currTest = this.currentChapterTests[i];
574
575     var option = document.createElement('option');
576     option.innerText = currTest.id;
577     option.className = currTest[statusProperty];
578     option._test = currTest;
579     testList.appendChild(option);
580   }
581 }
582
583 TestSuite.prototype.updateTestList = function()
584 {
585   var statusProperty = this.format == 'html4' ? 'statusHTML' : 'statusXHTML';
586   var testList = document.getElementById('test-list');
587   
588   var options = testList.getElementsByTagName('option');
589   for (var i = 0; i < options.length; ++i) {
590     var currOption = options[i];
591     currOption.className = currOption._test[statusProperty];
592   }
593 }
594
595 TestSuite.prototype.setSelectedChapter = function(chapter)
596 {
597   this.currentChapter = chapter;
598   this.buildTestListForChapter(this.currentChapter);
599   this.currChapterTestIndex = -1;
600   
601   this.fillTestList();
602   this.goToTestIndex(0);
603   
604   var chaptersPopup = document.getElementById('chapters');
605   chaptersPopup.selectedIndex = this.indexOfChapter(chapter);
606 }
607
608 /* ------------------------------------------------------- */
609
610 TestSuite.prototype.passTest = function()
611 {
612   this.recordResult(this.currentTestName(), 'pass');
613   this.nextTest();
614 }
615
616 TestSuite.prototype.failTest = function()
617 {
618   this.recordResult(this.currentTestName(), 'fail');
619   this.nextTest();
620 }
621
622 TestSuite.prototype.invalidTest = function()
623 {
624   this.recordResult(this.currentTestName(), 'invalid');
625   this.nextTest();
626 }
627
628 TestSuite.prototype.skipTest = function(reason)
629 {
630   this.recordResult(this.currentTestName(), 'skipped', reason);
631   this.nextTest();
632 }
633
634 TestSuite.prototype.nextTest = function()
635 {
636   if (this.currChapterTestIndex < this.currentChapterTests.length - 1)
637     this.goToTestIndex(this.currChapterTestIndex + 1);
638   else {
639     var currChapterIndex = this.indexOfChapter(this.currentChapter);
640     this.goToChapterIndex(currChapterIndex + 1);
641   }
642 }
643
644 TestSuite.prototype.previousTest = function()
645 {
646   if (this.currChapterTestIndex > 0)
647     this.goToTestIndex(this.currChapterTestIndex - 1);
648   else {
649     var currChapterIndex = this.indexOfChapter(this.currentChapter);
650     if (currChapterIndex > 0)
651       this.goToChapterIndex(currChapterIndex - 1);
652   }
653 }
654
655 TestSuite.prototype.goToNextIncompleteTest = function()
656 {
657   var completedProperty = this.format == 'html4' ? 'completedHTML' : 'completedXHTML';
658
659   // Look to the end of this chapter.
660   for (var i = this.currChapterTestIndex + 1; i < this.currentChapterTests.length; ++i) {
661     if (!this.currentChapterTests[i][completedProperty]) {
662       this.goToTestIndex(i);
663       return;
664     }
665   }
666
667   // Start looking through later chapter
668   var currChapterIndex = this.indexOfChapter(this.currentChapter);
669   for (var c = currChapterIndex + 1; c < kChapterData.length; ++c) {
670     var chapterData = this.chapterAtIndex(c);
671     
672     var testIndex = this.firstIncompleteTestIndex(chapterData);
673     if (testIndex != -1) {
674       this.goToChapterIndex(c);
675       this.goToTestIndex(testIndex);
676       break;
677     }
678   }
679 }
680
681 TestSuite.prototype.firstIncompleteTestIndex = function(chapter)
682 {
683   var completedProperty = this.format == 'html4' ? 'completedHTML' : 'completedXHTML';
684
685   var chapterTests = this.testListForChapter(chapter);
686   for (var i = 0; i < chapterTests.length; ++i) {
687     if (!chapterTests[i][completedProperty])
688       return i;
689   }
690   
691   return -1;
692 }
693
694 /* ------------------------------------------------------- */
695
696 TestSuite.prototype.goToTestByName = function(testName)
697 {
698   var match = testName.match(/^(?:(html4|xhtml1)\/)?([\w-_]+)(\.xht|\.htm)?/);
699   if (!match)
700     return false;
701
702   var prefix = match[1];
703   var testId = match[2];
704   var extension = match[3];
705   
706   var format = this.format;
707   if (prefix)
708     format = prefix;
709   else if (extension) {
710     if (extension == kXHTML1Data.suffix)
711       format = kXHTML1Data.path;
712     else if (extension == kHTML4Data.suffix)
713       format = kHTML4Data.path;
714   }
715   
716   this.switchToFormat(format);
717   
718   var test = this.tests[testId];
719   if (!test)
720     return false;
721
722   // Find the first chapter.
723   var links = test.links.split(',');
724   if (links.length == 0) {
725     window.console.log('test ' + test.id + 'had no links.');
726     return false;
727   }
728
729   var firstLink = links[0];
730   var result = firstLink.match(/^([.\w]+)(#.+)?$/);
731   if (result)
732     firstLink = result[1];
733
734   // Find the chapter and index of the test.
735   for (var i = 0; i < kChapterData.length; ++i) {
736     var chapterData = kChapterData[i];
737     if (chapterData.file == firstLink) {
738
739       this.goToChapterIndex(i);
740       
741       for (var j = 0; j < this.currentChapterTests.length; ++j) {
742         var currTest = this.currentChapterTests[j];
743         if (currTest.id == testId) {
744           this.goToTestIndex(j);
745           return true;
746         }
747       }
748     }
749   }
750
751   return false;
752 }
753
754 TestSuite.prototype.goToTestIndex = function(index)
755 {
756   if (index >= 0 && index < this.currentChapterTests.length) {
757     this.currChapterTestIndex = index;
758     this.loadCurrentTest();
759   }
760 }
761
762 TestSuite.prototype.goToChapterIndex = function(chapterIndex)
763 {
764   if (chapterIndex >= 0 && chapterIndex < kChapterData.length) {
765     var chapterFile = kChapterData[chapterIndex].file;
766     this.setSelectedChapter(this.chapters[chapterFile]);
767   }
768 }
769
770 TestSuite.prototype.currentTestName = function()
771 {
772   if (this.currChapterTestIndex < 0 || this.currChapterTestIndex >= this.currentChapterTests.length)
773     return undefined;
774
775   return this.currentChapterTests[this.currChapterTestIndex].id;
776 }
777
778 TestSuite.prototype.loadCurrentTest = function()
779 {
780   var theTest = this.currentChapterTests[this.currChapterTestIndex];
781   if (!theTest) {
782     this.configureForManualTest();
783     this.clearTest();
784     return;
785   }
786
787   if (theTest.reference) {
788     this.configureForRefTest();
789     this.loadRef(theTest);
790   } else {
791     this.configureForManualTest();
792   }
793
794   this.loadTest(theTest);
795
796   this.updateProgressLabel();
797   
798   document.getElementById('test-list').selectedIndex = this.currChapterTestIndex;
799 }
800
801 TestSuite.prototype.updateProgressLabel = function()
802 {
803   document.getElementById('test-index').innerText = this.currChapterTestIndex + 1;
804   document.getElementById('chapter-test-count').innerText = this.currentChapterTests.length;
805 }
806
807 TestSuite.prototype.configureForRefTest = function()
808 {
809   $('#test-content').addClass('with-ref');
810 }
811
812 TestSuite.prototype.configureForManualTest = function()
813 {
814   $('#test-content').removeClass('with-ref');
815 }
816
817 TestSuite.prototype.loadTest = function(test)
818 {
819   var iframe = document.getElementById('test-frame');
820   iframe.src = 'about:blank';
821   
822   var url = this.urlForTest(test.id);
823   window.setTimeout(function() {
824     iframe.src = url;
825   }, 0);
826   
827   document.getElementById('test-title').innerText = test.title;
828   document.getElementById('test-url').innerText = this.pathForTest(test.id);
829   document.getElementById('test-assertion').innerText = test.assertion;
830   document.getElementById('test-flags').innerText = test.flags;
831   
832   this.processFlags(test);
833 }
834
835 TestSuite.prototype.processFlags = function(test)
836
837   if (test.paged)
838     $('#test-content').addClass('print');
839   else
840     $('#test-content').removeClass('print');
841
842   var showWarning = false;
843   var warning = '';
844   if (test.flags.indexOf('font') != -1)
845     warning = 'Requires a specific font to be installed.';
846   
847   if (test.flags.indexOf('http') != -1) {
848     if (warning != '')
849       warning += ' ';
850     warning += 'Must be tested over HTTP, with custom HTTP headers.';
851   }
852   
853   if (test.paged) {
854     if (warning != '')
855       warning += ' ';
856     warning += 'Test via the browser\'s Print Preview.';
857   }
858
859   document.getElementById('warning').innerText = warning;
860
861   if (warning.length > 0)
862     $('#test-content').addClass('warn');
863   else
864     $('#test-content').removeClass('warn');
865
866 }
867
868 TestSuite.prototype.clearTest = function()
869 {
870   var iframe = document.getElementById('test-frame');
871   iframe.src = 'about:blank';
872   
873   document.getElementById('test-title').innerText = '';
874   document.getElementById('test-url').innerText = '';
875   document.getElementById('test-assertion').innerText = '';
876   document.getElementById('test-flags').innerText = '';
877
878   $('#test-content').removeClass('print');
879   $('#test-content').removeClass('warn');
880   document.getElementById('warning').innerText = '';
881 }
882
883 TestSuite.prototype.loadRef = function(test)
884 {
885   // Suites 20101001 and earlier used .xht refs, even for HTML tests, so strip off
886   // the extension and use the same format as the test.
887   var ref = test.reference.replace(/(\.xht)?$/, '');
888   
889   var iframe = document.getElementById('ref-frame');
890   iframe.src = this.urlForTest(ref);
891 }
892
893 TestSuite.prototype.pathForTest = function(testName)
894 {
895   var prefix = this.formatInfo.path;
896   var suffix = this.formatInfo.suffix;
897   
898   return prefix + '/' + testName + suffix;
899 }
900
901 TestSuite.prototype.urlForTest = function(testName)
902 {
903   return kTestSuiteHome + this.pathForTest(testName);
904 }
905
906 /* ------------------------------------------------------- */
907
908 TestSuite.prototype.recordResult = function(testName, resolution, comment)
909 {
910   if (!testName)
911     return;
912
913   this.beginAppendingOutput();
914   this.appendResultToOutput(this.formatInfo, testName, resolution, comment);
915   this.endAppendingOutput();
916   
917   if (comment == undefined)
918     comment = '';
919   
920   this.storeTestResult(testName, this.format, resolution, comment, navigator.userAgent);
921   
922   var htmlStatus = null;
923   var xhtmlStatus = null;
924   if (this.format == 'html4')
925     htmlStatus = resolution;
926   if (this.format == 'xhtml1')
927     xhtmlStatus = resolution;
928
929   this.markTestCompleted(testName, htmlStatus, xhtmlStatus);
930   this.updateTestList();
931
932   this.updateSummaryData();
933   this.updateChapterPopup();
934 }
935
936 TestSuite.prototype.beginAppendingOutput = function()
937 {
938 }
939
940 TestSuite.prototype.endAppendingOutput = function()
941 {
942   var output = document.getElementById('output');
943   output.scrollTop = output.scrollHeight;
944 }
945
946 TestSuite.prototype.appendResultToOutput = function(formatData, testName, resolution, comment)
947 {
948   var output = document.getElementById('output');
949   
950   var result = formatData.path + '/' + testName + formatData.suffix + '\t' + resolution;
951   if (comment)
952     result += '\t(' + comment + ')';
953
954   var line = document.createElement('p');
955   line.className = resolution;
956   line.appendChild(document.createTextNode(result));
957   output.appendChild(line);
958 }
959
960 TestSuite.prototype.clearOutput = function()
961 {
962   document.getElementById('output').innerHTML = '';
963 }
964
965 /* ------------------------------------------------------- */
966
967 TestSuite.prototype.switchToFormat = function(formatString)
968 {
969   if (formatString == 'html4')
970     document.harness.format.html4.checked = true;
971   else
972     document.harness.format.xhtml1.checked = true;
973
974   this.formatChanged(formatString);
975 }
976
977 TestSuite.prototype.formatChanged = function(formatString)
978 {
979   if (this.format == formatString)
980     return;
981   
982   this.format = formatString;
983
984   if (formatString == 'html4')
985     this.formatInfo = kHTML4Data;
986   else
987     this.formatInfo = kXHTML1Data;
988
989   // try to keep the current test selected
990   var selectedTestName;
991   if (this.currChapterTestIndex >= 0 && this.currChapterTestIndex < this.currentChapterTests.length)
992     selectedTestName = this.currentChapterTests[this.currChapterTestIndex].id;
993   
994   if (this.currentChapter) {
995     this.buildTestListForChapter(this.currentChapter);
996     this.fillTestList();
997     this.goToTestByName(selectedTestName);
998   }
999
1000   this.updateChapterPopup();
1001   this.updateTestList();
1002   this.updateProgressLabel();
1003 }
1004
1005 /* ------------------------------------------------------- */
1006
1007 TestSuite.prototype.asyncLoad = function(url, type, handler)
1008 {
1009   $.get(url, handler, type);
1010 }
1011
1012 /* ------------------------------------------------------- */
1013
1014 TestSuite.prototype.exportResults = function(resultTypeIndex)
1015 {
1016   var resultInfo = kResultsSelector[resultTypeIndex];
1017   if (!resultInfo)
1018     return;
1019
1020   resultInfo.exporter(this);
1021 }
1022
1023 TestSuite.prototype.exportHeader = function()
1024 {
1025   var result = '# Safari 5.0.2' + ' ' + navigator.platform + '\n';
1026   result += '# ' + navigator.userAgent + '\n';
1027   result += '# http://test.csswg.org/suites/css2.1/' + kTestSuiteVersion + '/\n';
1028   result += 'testname\tresult\n';
1029
1030   return result;
1031 }
1032
1033 TestSuite.prototype.createExportLine = function(formatData, testName, resolution, comment)
1034 {
1035   var result = formatData.path + '/' + testName + '\t' + resolution;
1036   if (comment)
1037     result += '\t(' + comment + ')';
1038   return result;
1039 }
1040
1041 TestSuite.prototype.exportQueryComplete = function(data)
1042 {
1043   window.open("data:text/plain," + escape(data))
1044 }
1045
1046 TestSuite.prototype.resultsPopupChanged = function(index)
1047 {
1048   var resultInfo = kResultsSelector[index];
1049   if (!resultInfo)
1050     return;
1051
1052   this.clearOutput();
1053   resultInfo.handler(this);
1054   
1055   var enableExport = resultInfo.exporter != undefined;
1056   document.getElementById('export-button').disabled = !enableExport;
1057 }
1058
1059 /* ------------------------- Import ------------------------------- */
1060 /*
1061   Import format is the same as the export format, namely:
1062   
1063   testname<tab>result
1064
1065   with optional trailing <tab>comment.
1066
1067 html4/absolute-non-replaced-height-002<tab>pass
1068 xhtml1/absolute-non-replaced-height-002<tab>?
1069   
1070   Lines starting with # are ignored.
1071   The "testname<tab>result" line is ignored.
1072 */
1073 TestSuite.prototype.importResults = function(data)
1074 {
1075   var testsToImport = [];
1076
1077   var lines = data.split('\n');
1078   for (var i = 0; i < lines.length; ++i) {
1079     var currLine = lines[i];
1080     if (currLine.length == 0 || currLine.charAt(0) == '#')
1081       continue;
1082
1083     var match = currLine.match(/^(html4|xhtml1)\/([\w-_]+)\t([\w?]+)\t?(.+)?$/);
1084     if (match) {
1085       var test = { 'id' : match[2] };
1086       test.format =  match[1];
1087       test.result = match[3];
1088       test.comment = match[4];
1089       
1090       if (test.result != '?')
1091         testsToImport.push(test);
1092     } else {
1093       window.console.log('failed to match line \'' + currLine + '\'');
1094     }
1095   }
1096
1097   this.importTestResults(testsToImport);
1098   
1099   this.resetTestStatus();
1100   this.updateSummaryData();
1101 }
1102
1103
1104
1105 /* --------------------- Clear Results --------------------------- */
1106 /*
1107   Clear results format is either same as the export format, or
1108   a list of bare test IDs (e.g. absolute-non-replaced-height-001)
1109   in which case both HTML4 and XHTML1 results are cleared.
1110 */
1111 TestSuite.prototype.clearResults = function(data)
1112 {
1113   var testsToClear = [];
1114
1115   var lines = data.split('\n');
1116   for (var i = 0; i < lines.length; ++i) {
1117     var currLine = lines[i];
1118     if (currLine.length == 0 || currLine.charAt(0) == '#')
1119       continue;
1120
1121     // Look for format/test with possible extension
1122     var result = currLine.match(/^((html4|xhtml1)?)\/?([\w-_]+)/);
1123     if (result) {
1124       var testId = result[3];
1125       var format = result[1];
1126       
1127       var clearHTML = format.length == 0 || format == 'html4';
1128       var clearXHTML = format.length == 0 || format == 'xhtml1';
1129       
1130       var result = { 'id' : testId };
1131       result.clearHTML = clearHTML;
1132       result.clearXHTML = clearXHTML;
1133
1134       testsToClear.push(result);
1135     } else {
1136       window.console.log('failed to match line ' + currLine);
1137     }
1138   }
1139   
1140   this.clearTestResults(testsToClear);
1141   
1142   this.resetTestStatus();
1143   this.updateSummaryData();
1144 }
1145
1146 /* -------------------------------------------------------- */
1147
1148 TestSuite.prototype.exportResultsCompletion = function(exportTests)
1149 {
1150   // Lame workaround for ORDER BY not working
1151   exportTests.sort(function(a, b) {
1152       return a.test.localeCompare(b.test);
1153   });
1154   
1155   var exportLines = [];
1156   for (var i = 0; i < exportTests.length; ++i) {
1157     var currTest = exportTests[i];
1158     if (currTest.html4 != '')
1159       exportLines.push(currTest.html4);
1160     if (currTest.xhtml1 != '')
1161       exportLines.push(currTest.xhtml1);
1162   }
1163   
1164   var exportString = this.exportHeader() + exportLines.join('\n');
1165   this.exportQueryComplete(exportString);
1166 }
1167
1168 /* -------------------------------------------------------- */
1169
1170 TestSuite.prototype.showResultsForCompletedTests = function()
1171 {
1172   this.beginAppendingOutput();
1173   
1174   var _self = this;
1175   this.queryDatabaseForCompletedTests(
1176       function(item) {
1177         if (item.hstatus)
1178           _self.appendResultToOutput(kHTML4Data, item.test, item.hstatus, item.hcomment);
1179
1180         if (item.xstatus)
1181           _self.appendResultToOutput(kXHTML1Data, item.test, item.xstatus, item.xcomment);
1182       },
1183       function() {
1184         _self.endAppendingOutput();
1185       }
1186     );
1187 }
1188
1189 TestSuite.prototype.exportResultsForCompletedTests = function()
1190 {
1191   var exportTests = []; // each test will have html and xhtml items on it
1192
1193   var _self = this;
1194   this.queryDatabaseForCompletedTests(
1195       function(item) {
1196         var htmlLine = '';
1197         if (item.hstatus)
1198           htmlLine= _self.createExportLine(kHTML4Data, item.test, item.hstatus, item.hcomment);
1199
1200         var xhtmlLine = '';
1201         if (item.xstatus)
1202           xhtmlLine = _self.createExportLine(kXHTML1Data, item.test, item.xstatus, item.xcomment);
1203
1204         exportTests.push({
1205           'test' : item.test,
1206           'html4' : htmlLine,
1207           'xhtml1' : xhtmlLine });
1208       },
1209       function() {
1210         _self.exportResultsCompletion(exportTests);
1211       }
1212     );
1213 }
1214
1215
1216 /* -------------------------------------------------------- */
1217
1218 TestSuite.prototype.showResultsForAllTests = function()
1219 {
1220   this.beginAppendingOutput();
1221
1222   var _self = this;
1223   this.queryDatabaseForAllTests('test',
1224     function(item) {
1225       _self.appendResultToOutput(kHTML4Data, item.test, item.hstatus, item.hcomment);
1226       _self.appendResultToOutput(kXHTML1Data, item.test, item.xstatus, item.xcomment);
1227     },
1228     function() {
1229       _self.endAppendingOutput();
1230     });
1231 }
1232
1233 TestSuite.prototype.exportResultsForAllTests = function()
1234 {
1235   var exportTests = [];
1236
1237   var _self = this;
1238   this.queryDatabaseForAllTests('test',
1239       function(item) {
1240         var htmlLine= _self.createExportLine(kHTML4Data, item.test, item.hstatus ? item.hstatus : '?', item.hcomment);
1241         var xhtmlLine = _self.createExportLine(kXHTML1Data, item.test, item.xstatus ? item.xstatus : '?', item.xcomment);
1242         exportTests.push({
1243           'test' : item.test,
1244           'html4' : htmlLine,
1245           'xhtml1' : xhtmlLine });
1246       },
1247       function() {
1248         _self.exportResultsCompletion(exportTests);
1249       }
1250     );
1251 }
1252
1253 /* -------------------------------------------------------- */
1254
1255 TestSuite.prototype.showResultsForTestsNotRun = function()
1256 {
1257   this.beginAppendingOutput();
1258
1259   var _self = this;
1260   this.queryDatabaseForTestsNotRun(
1261       function(item) {
1262         if (!item.hstatus)
1263           _self.appendResultToOutput(kHTML4Data, item.test, '?', item.hcomment);
1264         if (!item.xstatus)
1265           _self.appendResultToOutput(kXHTML1Data, item.test, '?', item.xcomment);
1266       },
1267       function() {
1268         _self.endAppendingOutput();
1269       }
1270     );
1271 }
1272
1273 TestSuite.prototype.exportResultsForTestsNotRun = function()
1274 {
1275   var exportTests = [];
1276
1277   var _self = this;
1278   this.queryDatabaseForTestsNotRun(
1279       function(item) {
1280         var htmlLine = '';
1281         if (!item.hstatus)
1282           htmlLine= _self.createExportLine(kHTML4Data, item.test, '?', item.hcomment);
1283
1284         var xhtmlLine = '';
1285         if (!item.xstatus)
1286           xhtmlLine = _self.createExportLine(kXHTML1Data, item.test, '?', item.xcomment);
1287
1288         exportTests.push({
1289           'test' : item.test,
1290           'html4' : htmlLine,
1291           'xhtml1' : xhtmlLine });
1292       },
1293       function() {
1294         _self.exportResultsCompletion(exportTests);
1295       }
1296     );
1297 }
1298
1299 /* -------------------------------------------------------- */
1300
1301 TestSuite.prototype.showResultsForTestsWithStatus = function(status)
1302 {
1303   this.beginAppendingOutput();
1304
1305   var _self = this;
1306   this.queryDatabaseForTestsWithStatus(status,
1307       function(item) {
1308         if (item.hstatus == status)
1309           _self.appendResultToOutput(kHTML4Data, item.test, item.hstatus, item.hcomment);
1310         if (item.xstatus == status)
1311           _self.appendResultToOutput(kXHTML1Data, item.test, item.xstatus, item.xcomment);
1312       },
1313       function() {
1314         _self.endAppendingOutput();
1315       }
1316     );
1317 }
1318
1319 TestSuite.prototype.exportResultsForTestsWithStatus = function(status)
1320 {
1321   var exportTests = [];
1322
1323   var _self = this;
1324   this.queryDatabaseForTestsWithStatus(status,
1325       function(item) {
1326         var htmlLine = '';
1327         if (item.hstatus == status)
1328           htmlLine= _self.createExportLine(kHTML4Data, item.test, item.hstatus, item.hcomment);
1329
1330         var xhtmlLine = '';
1331         if (item.xstatus == status)
1332           xhtmlLine = _self.createExportLine(kXHTML1Data, item.test, item.xstatus, item.xcomment);
1333
1334         exportTests.push({
1335           'test' : item.test,
1336           'html4' : htmlLine,
1337           'xhtml1' : xhtmlLine });
1338       },
1339       function() {
1340         _self.exportResultsCompletion(exportTests);
1341       }
1342     );
1343 }
1344
1345 /* -------------------------------------------------------- */
1346
1347 TestSuite.prototype.showResultsForTestsWithMismatchedResults = function()
1348 {
1349   this.beginAppendingOutput();
1350
1351   var _self = this;
1352   this.queryDatabaseForTestsWithMixedStatus(
1353       function(item) {
1354         _self.appendResultToOutput(kHTML4Data, item.test, item.hstatus, item.hcomment);
1355         _self.appendResultToOutput(kXHTML1Data, item.test, item.xstatus, item.xcomment);
1356       },
1357       function() {
1358         _self.endAppendingOutput();
1359       }
1360     );
1361 }
1362
1363 TestSuite.prototype.exportResultsForTestsWithMismatchedResults = function()
1364 {
1365   var exportTests = [];
1366
1367   var _self = this;
1368   this.queryDatabaseForTestsWithMixedStatus(
1369       function(item) {
1370         var htmlLine= _self.createExportLine(kHTML4Data, item.test, item.hstatus ? item.hstatus : '?', item.hcomment);
1371         var xhtmlLine = _self.createExportLine(kXHTML1Data, item.test, item.xstatus ? item.xstatus : '?', item.xcomment);
1372         exportTests.push({
1373           'test' : item.test,
1374           'html4' : htmlLine,
1375           'xhtml1' : xhtmlLine });
1376       },
1377       function() {
1378         _self.exportResultsCompletion(exportTests);
1379       }
1380     );
1381 }
1382
1383 /* -------------------------------------------------------- */
1384
1385 TestSuite.prototype.markTestCompleted = function(testID, htmlStatus, xhtmlStatus)
1386 {
1387   var test = this.tests[testID];
1388   if (!test) {
1389     window.console.log('markTestCompleted failed to find test ' + testID);
1390     return;
1391   }
1392
1393   if (htmlStatus) {
1394     test.completedHTML = true;
1395     test.statusHTML = htmlStatus;
1396   }
1397   if (xhtmlStatus) {
1398     test.completedXHTML = true;
1399     test.statusXHTML = xhtmlStatus;
1400   }
1401 }
1402
1403 TestSuite.prototype.testCompletionStateChanged = function()
1404 {
1405   this.updateTestList();
1406   this.updateChapterPopup();
1407 }
1408
1409 TestSuite.prototype.loadTestStatus = function()
1410 {
1411   var _self = this;
1412   this.queryDatabaseForCompletedTests(
1413       function(item) {
1414       _self.markTestCompleted(item.test, item.hstatus, item.xstatus);
1415       },
1416       function() {
1417         _self.testCompletionStateChanged();
1418       }
1419     );
1420     
1421     this.updateChapterPopup();
1422 }
1423
1424 TestSuite.prototype.resetTestStatus = function()
1425 {
1426   for (var testID in this.tests) {
1427     var currTest = this.tests[testID];
1428     currTest.completedHTML = false;
1429     currTest.completedXHTML = false;
1430   }
1431   this.loadTestStatus();
1432 }
1433
1434 /* -------------------------------------------------------- */
1435
1436 TestSuite.prototype.updateSummaryData = function()
1437 {
1438   this.queryDatabaseForSummary(
1439       function(results) {
1440         
1441         var hTotal, xTotal;
1442         var hDone, xDone;
1443         
1444         for (var i = 0; i < results.length; ++i) {
1445           var result = results[i];
1446           
1447           switch (result.name) {
1448             case 'h-total': hTotal = result.count; break;
1449             case 'x-total': xTotal = result.count; break;
1450             case 'h-tested': hDone = result.count; break;
1451             case 'x-tested': xDone = result.count; break;
1452           }
1453
1454           document.getElementById(result.name).innerText = result.count;
1455         }
1456         
1457         // We should get these all together.
1458         if (hTotal) {
1459           document.getElementById('h-percent').innerText = Math.round(100.0 * hDone / hTotal);
1460           document.getElementById('x-percent').innerText = Math.round(100.0 * xDone / xTotal);
1461         }
1462       }
1463     );
1464 }
1465
1466 /* ------------------------------------------------------- */
1467 // Database stuff
1468
1469 function errorHandler(transaction, error)
1470 {
1471   alert('Database error: ' + error.message);
1472   window.console.log('Database error: ' + error.message);
1473 }
1474
1475 TestSuite.prototype.openDatabase = function()
1476 {
1477   if (!'openDatabase' in window) {
1478     alert('Your browser does not support client-side SQL databases, so results will not be stored.');
1479     return;
1480   }
1481   
1482   var _self = this;
1483   this.db = window.openDatabase('css21testsuite', '', 'CSS 2.1 test suite results', 10 * 1024 * 1024);
1484
1485   // Migration handling. We assume migration will happen whenever the suite version changes,
1486   // so that we can check for new or obsoleted tests.
1487   function creation(tx) {
1488     _self.databaseCreated(tx);
1489   }
1490
1491   function migration1_0To1_1(tx) {
1492     window.console.log('updating 1.0 to 1.1');
1493     // We'll use the 'seen' column to cross-check with testinfo.data.
1494     tx.executeSql('ALTER TABLE tests ADD COLUMN seen BOOLEAN DEFAULT \"FALSE\"', null, function() {
1495       _self.syncDatabaseWithTestInfoData();
1496     }, errorHandler);
1497   }
1498
1499   if (this.db.version == '') {
1500     _self.db.changeVersion('', '1.0', creation, null, function() {
1501       _self.db.changeVersion('1.0', '1.1', migration1_0To1_1, null, function() {
1502         _self.databaseReady();
1503       }, errorHandler);
1504     }, errorHandler);
1505
1506     return;
1507   }
1508
1509   if (this.db.version == '1.0') {
1510     _self.db.changeVersion('1.0', '1.1', migration1_0To1_1, null, function() {
1511       window.console.log('ready')
1512       _self.databaseReady();
1513     }, errorHandler);
1514     return;
1515   }
1516
1517   this.databaseReady();
1518 }
1519
1520 TestSuite.prototype.databaseCreated = function(tx)
1521 {
1522   window.console.log('databaseCreated');
1523   this.populatingDatabase = true;
1524
1525   // hstatus: HTML4 result
1526   // xstatus: XHTML1 result
1527   var _self = this;
1528   tx.executeSql('CREATE TABLE tests (test PRIMARY KEY UNIQUE, ref, title, flags, links, assertion, hstatus, hcomment, xstatus, xcomment)', null,
1529     function(tx, results) {
1530       _self.populateDatabaseFromTestInfoData();
1531     }, errorHandler);
1532 }
1533
1534 TestSuite.prototype.databaseReady = function()
1535 {
1536   this.updateSummaryData();
1537   this.loadTestStatus();
1538 }
1539
1540 TestSuite.prototype.storeTestResult = function(test, format, result, comment, useragent)
1541 {
1542   if (!this.db)
1543     return;
1544
1545   this.db.transaction(function (tx) {
1546     if (format == 'html4')
1547       tx.executeSql('UPDATE tests SET hstatus=?, hcomment=? WHERE test=?\n', [result, comment, test], null, errorHandler);
1548     else if (format == 'xhtml1')
1549       tx.executeSql('UPDATE tests SET xstatus=?, xcomment=? WHERE test=?\n', [result, comment, test], null, errorHandler);
1550   });
1551 }
1552
1553 TestSuite.prototype.importTestResults = function(results)
1554 {
1555   if (!this.db)
1556     return;
1557
1558   this.db.transaction(function (tx) {
1559
1560     for (var i = 0; i < results.length; ++i) {
1561       var currResult = results[i];
1562
1563       var query;
1564       if (currResult.format == 'html4')
1565         query = 'UPDATE tests SET hstatus=?, hcomment=? WHERE test=?\n';
1566       else if (currResult.format == 'xhtml1')
1567         query = 'UPDATE tests SET xstatus=?, xcomment=? WHERE test=?\n';
1568
1569       tx.executeSql(query, [currResult.result, currResult.comment, currResult.id], null, errorHandler);
1570     }
1571   });
1572 }
1573
1574 TestSuite.prototype.clearTestResults = function(results)
1575 {
1576   if (!this.db)
1577     return;
1578
1579   this.db.transaction(function (tx) {
1580     
1581     for (var i = 0; i < results.length; ++i) {
1582       var currResult = results[i];
1583
1584       if (currResult.clearHTML)
1585         tx.executeSql('UPDATE tests SET hstatus=NULL, hcomment=NULL WHERE test=?\n', [currResult.id], null, errorHandler);
1586
1587       if (currResult.clearXHTML)
1588         tx.executeSql('UPDATE tests SET xstatus=NULL, xcomment=NULL WHERE test=?\n', [currResult.id], null, errorHandler);
1589       
1590     }
1591   });
1592 }
1593
1594 TestSuite.prototype.populateDatabaseFromTestInfoData = function()
1595 {
1596   if (!this.testInfoLoaded) {
1597     window.console.log('Tring to populate database before testinfo.data has been loaded');
1598     return;
1599   }
1600   
1601   window.console.log('populateDatabaseFromTestInfoData')
1602   var _self = this;
1603   this.db.transaction(function (tx) {
1604     for (var testID in _self.tests) {
1605       var test = _self.tests[testID];
1606       // Version 1.0, so no 'seen' column.
1607       tx.executeSql('INSERT INTO tests (test, ref, title, flags, links, assertion) VALUES (?, ?, ?, ?, ?, ?)',
1608         [test.id, test.reference, test.title, test.flags, test.links, test.assertion], null, errorHandler);
1609     }
1610     _self.populatingDatabase = false;
1611   });
1612
1613 }
1614
1615 TestSuite.prototype.insertTest = function(tx, test)
1616 {
1617   tx.executeSql('INSERT INTO tests (test, ref, title, flags, links, assertion, seen) VALUES (?, ?, ?, ?, ?, ?, ?)',
1618     [test.id, test.reference, test.title, test.flags, test.links, test.assertion, 'TRUE'], null, errorHandler);
1619 }
1620
1621 // Deal with removed/renamed tests in a new version of the suite.
1622 // self.tests is canonical; the database may contain stale entries.
1623 TestSuite.prototype.syncDatabaseWithTestInfoData = function()
1624 {
1625   if (!this.testInfoLoaded) {
1626     window.console.log('Trying to sync database before testinfo.data has been loaded');
1627     return;
1628   }
1629
1630   // Make an object with all tests that we'll use to track new tests.
1631   var testsToInsert = {};
1632   for (var testId in this.tests) {
1633     var currTest = this.tests[testId];
1634     testsToInsert[currTest.id] = currTest;
1635   }
1636   
1637   var _self = this;
1638   this.db.transaction(function (tx) {
1639     // Find tests that are not in the database yet.
1640     // (Wasn't able to get INSERT ... IF NOT working.)
1641     tx.executeSql('SELECT * FROM tests', [], function(tx, results) {
1642       var len = results.rows.length;
1643       for (var i = 0; i < len; ++i) {
1644         var item = results.rows.item(i);
1645         delete testsToInsert[item.test];
1646       }
1647     }, errorHandler);
1648   });
1649
1650   this.db.transaction(function (tx) {
1651     for (var testId in testsToInsert) {
1652       var currTest = testsToInsert[testId];
1653       window.console.log(currTest.id + ' is new; inserting');
1654       _self.insertTest(tx, currTest);
1655     }
1656   });
1657     
1658   this.db.transaction(function (tx) {
1659     for (var testID in _self.tests)
1660       tx.executeSql('UPDATE tests SET seen=\"TRUE\" WHERE test=?\n', [testID], null, errorHandler);
1661
1662     tx.executeSql('SELECT * FROM tests WHERE seen=\"FALSE\"', [], function(tx, results) {
1663       var len = results.rows.length;
1664       for (var i = 0; i < len; ++i) {
1665         var item = results.rows.item(i);
1666         window.console.log('Test ' + item.test + ' was in the database but is no longer in the suite; deleting.');
1667       }
1668     }, errorHandler);
1669
1670     // Delete rows for disappeared tests.
1671     tx.executeSql('DELETE FROM tests WHERE seen=\"FALSE\"', [], function(tx, results) {
1672       _self.populatingDatabase = false;
1673       _self.databaseReady();
1674     }, errorHandler);
1675   });
1676 }
1677
1678 TestSuite.prototype.queryDatabaseForAllTests = function(sortKey, perRowHandler, completionHandler)
1679 {
1680   if (this.populatingDatabase)
1681     return;
1682
1683   var _self = this;
1684   this.db.transaction(function (tx) {
1685     if (_self.populatingDatabase)
1686       return;
1687     var query;
1688     var args = [];
1689     if (sortKey != '') {
1690       query = 'SELECT * FROM tests ORDER BY ? ASC';  // ORDER BY doesn't seem to work
1691       args.push(sortKey);
1692     }
1693     else
1694       query = 'SELECT * FROM tests';
1695
1696     tx.executeSql(query, args, function(tx, results) {
1697
1698       var len = results.rows.length;
1699       for (var i = 0; i < len; ++i)
1700         perRowHandler(results.rows.item(i));
1701       
1702       completionHandler();
1703     }, errorHandler);
1704   });  
1705 }
1706
1707 TestSuite.prototype.queryDatabaseForTestsWithStatus = function(status, perRowHandler, completionHandler)
1708 {
1709   if (this.populatingDatabase)
1710     return;
1711
1712   var _self = this;
1713   this.db.transaction(function (tx) {
1714     if (_self.populatingDatabase)
1715       return;
1716     tx.executeSql('SELECT * FROM tests WHERE hstatus=? OR xstatus=?', [status, status], function(tx, results) {
1717
1718       var len = results.rows.length;
1719       for (var i = 0; i < len; ++i)
1720         perRowHandler(results.rows.item(i));
1721       
1722       completionHandler();
1723     }, errorHandler);
1724   });  
1725 }
1726
1727 TestSuite.prototype.queryDatabaseForTestsWithMixedStatus = function(perRowHandler, completionHandler)
1728 {
1729   if (this.populatingDatabase)
1730     return;
1731
1732   var _self = this;
1733   this.db.transaction(function (tx) {
1734     if (_self.populatingDatabase)
1735       return;
1736     tx.executeSql('SELECT * FROM tests WHERE hstatus IS NOT NULL AND xstatus IS NOT NULL AND hstatus <> xstatus', [], function(tx, results) {
1737
1738       var len = results.rows.length;
1739       for (var i = 0; i < len; ++i)
1740         perRowHandler(results.rows.item(i));
1741       
1742       completionHandler();
1743     }, errorHandler);
1744   });  
1745 }
1746
1747 TestSuite.prototype.queryDatabaseForCompletedTests = function(perRowHandler, completionHandler)
1748 {
1749   if (this.populatingDatabase)
1750     return;
1751
1752   var _self = this;
1753   this.db.transaction(function (tx) {
1754     
1755     if (_self.populatingDatabase)
1756       return;
1757
1758     tx.executeSql('SELECT * FROM tests WHERE hstatus IS NOT NULL OR xstatus IS NOT NULL', [], function(tx, results) {
1759       var len = results.rows.length;
1760       for (var i = 0; i < len; ++i)
1761         perRowHandler(results.rows.item(i));
1762       
1763       completionHandler();
1764     }, errorHandler);
1765   });  
1766 }
1767
1768 TestSuite.prototype.queryDatabaseForTestsNotRun = function(perRowHandler, completionHandler)
1769 {
1770   if (this.populatingDatabase)
1771     return;
1772
1773   var _self = this;
1774   this.db.transaction(function (tx) {
1775     if (_self.populatingDatabase)
1776       return;
1777
1778     tx.executeSql('SELECT * FROM tests WHERE hstatus IS NULL OR xstatus IS NULL', [], function(tx, results) {
1779
1780       var len = results.rows.length;
1781       for (var i = 0; i < len; ++i)
1782         perRowHandler(results.rows.item(i));
1783
1784       completionHandler();
1785     }, errorHandler);
1786   });  
1787 }
1788
1789 /*
1790
1791   completionHandler gets called an array of results,
1792   which may be some or all of:
1793   
1794   data = [
1795     { 'name' : ,
1796       'count' : 
1797     },
1798   ]
1799   
1800   where name is one of:
1801   
1802     'h-total'
1803     'h-tested'
1804     'h-passed'
1805     'h-failed'
1806     'h-skipped'
1807
1808     'x-total'
1809     'x-tested'
1810     'x-passed'
1811     'x-failed'
1812     'x-skipped'
1813
1814  */
1815
1816
1817 TestSuite.prototype.countTestsWithColumnValue = function(tx, completionHandler, column, value, label)
1818 {  
1819   var allRowsCount = 'COUNT(*)';
1820
1821   tx.executeSql('SELECT COUNT(*) FROM tests WHERE ' + column + '=?', [value], function(tx, results) {
1822     var data = [];
1823     if (results.rows.length > 0)
1824       data.push({ 'name' : label, 'count' : results.rows.item(0)[allRowsCount] })
1825     completionHandler(data);
1826   }, errorHandler);
1827 }
1828
1829 TestSuite.prototype.countTestsWithFlag = function(tx, completionHandler, flag)
1830 {  
1831   var allRowsCount = 'COUNT(*)';
1832
1833   tx.executeSql('SELECT COUNT(*) FROM tests WHERE flags LIKE \"%' + flag + '%\"', [], function(tx, results) {
1834     var rowCount = 0;
1835     if (results.rows.length > 0)
1836       rowCount = results.rows.item(0)[allRowsCount];
1837     completionHandler(rowCount);
1838   }, errorHandler);
1839 }
1840
1841 TestSuite.prototype.queryDatabaseForSummary = function(completionHandler)
1842 {
1843   if (!this.db || this.populatingDatabase)
1844     return;
1845
1846   var _self = this;
1847
1848   var htmlOnlyTestCount = 0;
1849   var xHtmlOnlyTestCount = 0;
1850
1851   this.db.transaction(function (tx) {
1852     if (_self.populatingDatabase)
1853       return;
1854
1855     var allRowsCount = 'COUNT(*)';
1856       
1857     _self.countTestsWithFlag(tx, function(count) {
1858       htmlOnlyTestCount = count;
1859     }, 'htmlOnly');
1860
1861     _self.countTestsWithFlag(tx, function(count) {
1862       xHtmlOnlyTestCount = count;
1863     }, 'nonHTML');
1864   });
1865   
1866   this.db.transaction(function (tx) {
1867     if (_self.populatingDatabase)
1868       return;
1869
1870     var allRowsCount = 'COUNT(*)';
1871     var html4RowsCount = 'COUNT(hstatus)';
1872     var xhtml1RowsCount = 'COUNT(xstatus)';
1873     
1874     tx.executeSql('SELECT COUNT(*), COUNT(hstatus), COUNT(xstatus) FROM tests', [], function(tx, results) {
1875
1876       var data = [];
1877       if (results.rows.length > 0) {
1878         var rowItem = results.rows.item(0);
1879         data.push({ 'name' : 'h-total' , 'count' : rowItem[allRowsCount] - xHtmlOnlyTestCount })
1880         data.push({ 'name' : 'x-total' , 'count' : rowItem[allRowsCount] - htmlOnlyTestCount })
1881         data.push({ 'name' : 'h-tested', 'count' : rowItem[html4RowsCount] })
1882         data.push({ 'name' : 'x-tested', 'count' : rowItem[xhtml1RowsCount] })
1883       }
1884       completionHandler(data);
1885       
1886     }, errorHandler);
1887
1888
1889     _self.countTestsWithColumnValue(tx, completionHandler, 'hstatus', 'pass', 'h-passed');
1890     _self.countTestsWithColumnValue(tx, completionHandler, 'xstatus', 'pass', 'x-passed');
1891
1892     _self.countTestsWithColumnValue(tx, completionHandler, 'hstatus', 'fail', 'h-failed');
1893     _self.countTestsWithColumnValue(tx, completionHandler, 'xstatus', 'fail', 'x-failed');
1894
1895     _self.countTestsWithColumnValue(tx, completionHandler, 'hstatus', 'skipped', 'h-skipped');
1896     _self.countTestsWithColumnValue(tx, completionHandler, 'xstatus', 'skipped', 'x-skipped');
1897
1898     _self.countTestsWithColumnValue(tx, completionHandler, 'hstatus', 'invalid', 'h-invalid');
1899     _self.countTestsWithColumnValue(tx, completionHandler, 'xstatus', 'invalid', 'x-invalid');
1900   });
1901 }
1902