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
28const kTestSuiteVersion = '20101001';
29const kTestSuiteHome = '../' + kTestSuiteVersion + '/';
30const kTestInfoDataFile = 'testinfo.data';
31
32const 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
140const kHTML4Data = {
141  'path' : 'html4',
142  'suffix' : '.htm'
143};
144
145const kXHTML1Data = {
146  'path' : 'xhtml1',
147  'suffix' : '.xht'
148};
149
150// Results popup
151const 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
194function 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
229Test.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
240Test.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
251Test.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
262function 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
276ChapterSection.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
292function 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
301Chapter.prototype.description = function(format)
302{
303
304
305  return this.title + ' (' + this.testCount(format) + ' tests, ' + this.untestedCount(format) + ' untested)';
306}
307
308Chapter.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
323Chapter.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
334Chapter.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
350String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }
351
352function 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
374TestSuite.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
382TestSuite.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
401TestSuite.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
413TestSuite.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
456TestSuite.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
467TestSuite.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
475TestSuite.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
492TestSuite.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
507TestSuite.prototype.buildTestListForChapter = function(chapter)
508{
509  this.currentChapterTests = this.testListForChapter(chapter);
510}
511
512TestSuite.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
534TestSuite.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
557TestSuite.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
565TestSuite.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
583TestSuite.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
595TestSuite.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
610TestSuite.prototype.passTest = function()
611{
612  this.recordResult(this.currentTestName(), 'pass');
613  this.nextTest();
614}
615
616TestSuite.prototype.failTest = function()
617{
618  this.recordResult(this.currentTestName(), 'fail');
619  this.nextTest();
620}
621
622TestSuite.prototype.invalidTest = function()
623{
624  this.recordResult(this.currentTestName(), 'invalid');
625  this.nextTest();
626}
627
628TestSuite.prototype.skipTest = function(reason)
629{
630  this.recordResult(this.currentTestName(), 'skipped', reason);
631  this.nextTest();
632}
633
634TestSuite.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
644TestSuite.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
655TestSuite.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
681TestSuite.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
696TestSuite.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
754TestSuite.prototype.goToTestIndex = function(index)
755{
756  if (index >= 0 && index < this.currentChapterTests.length) {
757    this.currChapterTestIndex = index;
758    this.loadCurrentTest();
759  }
760}
761
762TestSuite.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
770TestSuite.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
778TestSuite.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
801TestSuite.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
807TestSuite.prototype.configureForRefTest = function()
808{
809  $('#test-content').addClass('with-ref');
810}
811
812TestSuite.prototype.configureForManualTest = function()
813{
814  $('#test-content').removeClass('with-ref');
815}
816
817TestSuite.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
835TestSuite.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
868TestSuite.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
883TestSuite.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
893TestSuite.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
901TestSuite.prototype.urlForTest = function(testName)
902{
903  return kTestSuiteHome + this.pathForTest(testName);
904}
905
906/* ------------------------------------------------------- */
907
908TestSuite.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
936TestSuite.prototype.beginAppendingOutput = function()
937{
938}
939
940TestSuite.prototype.endAppendingOutput = function()
941{
942  var output = document.getElementById('output');
943  output.scrollTop = output.scrollHeight;
944}
945
946TestSuite.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
960TestSuite.prototype.clearOutput = function()
961{
962  document.getElementById('output').innerHTML = '';
963}
964
965/* ------------------------------------------------------- */
966
967TestSuite.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
977TestSuite.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
1007TestSuite.prototype.asyncLoad = function(url, type, handler)
1008{
1009  $.get(url, handler, type);
1010}
1011
1012/* ------------------------------------------------------- */
1013
1014TestSuite.prototype.exportResults = function(resultTypeIndex)
1015{
1016  var resultInfo = kResultsSelector[resultTypeIndex];
1017  if (!resultInfo)
1018    return;
1019
1020  resultInfo.exporter(this);
1021}
1022
1023TestSuite.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
1033TestSuite.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
1041TestSuite.prototype.exportQueryComplete = function(data)
1042{
1043  window.open("data:text/plain," + escape(data))
1044}
1045
1046TestSuite.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
1067html4/absolute-non-replaced-height-002<tab>pass
1068xhtml1/absolute-non-replaced-height-002<tab>?
1069
1070  Lines starting with # are ignored.
1071  The "testname<tab>result" line is ignored.
1072*/
1073TestSuite.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*/
1111TestSuite.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
1148TestSuite.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
1170TestSuite.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
1189TestSuite.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
1218TestSuite.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
1233TestSuite.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
1255TestSuite.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
1273TestSuite.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
1301TestSuite.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
1319TestSuite.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
1347TestSuite.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
1363TestSuite.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
1385TestSuite.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
1403TestSuite.prototype.testCompletionStateChanged = function()
1404{
1405  this.updateTestList();
1406  this.updateChapterPopup();
1407}
1408
1409TestSuite.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
1424TestSuite.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
1436TestSuite.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
1469function errorHandler(transaction, error)
1470{
1471  alert('Database error: ' + error.message);
1472  window.console.log('Database error: ' + error.message);
1473}
1474
1475TestSuite.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
1520TestSuite.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
1534TestSuite.prototype.databaseReady = function()
1535{
1536  this.updateSummaryData();
1537  this.loadTestStatus();
1538}
1539
1540TestSuite.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
1553TestSuite.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
1574TestSuite.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
1594TestSuite.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
1615TestSuite.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.
1623TestSuite.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
1678TestSuite.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
1707TestSuite.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
1727TestSuite.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
1747TestSuite.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
1768TestSuite.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
1817TestSuite.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
1829TestSuite.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
1841TestSuite.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