Home | History | Annotate | Download | only in harness
      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 
   1903