1/*global add_completion_callback, setup */ 2/* 3 * This file is intended for vendors to implement 4 * code needed to integrate testharness.js tests with their own test systems. 5 * 6 * The default implementation extracts metadata from the tests and validates 7 * it against the cached version that should be present in the test source 8 * file. If the cache is not found or is out of sync, source code suitable for 9 * caching the metadata is optionally generated. 10 * 11 * The cached metadata is present for extraction by test processing tools that 12 * are unable to execute javascript. 13 * 14 * Metadata is attached to tests via the properties parameter in the test 15 * constructor. See testharness.js for details. 16 * 17 * Typically test system integration will attach callbacks when each test has 18 * run, using add_result_callback(callback(test)), or when the whole test file 19 * has completed, using 20 * add_completion_callback(callback(tests, harness_status)). 21 * 22 * For more documentation about the callback functions and the 23 * parameters they are called with see testharness.js 24 */ 25 26 27 28var metadata_generator = { 29 30 currentMetadata: {}, 31 cachedMetadata: false, 32 metadataProperties: ['help', 'assert', 'author'], 33 34 error: function(message) { 35 var messageElement = document.createElement('p'); 36 messageElement.setAttribute('class', 'error'); 37 this.appendText(messageElement, message); 38 39 var summary = document.getElementById('summary'); 40 if (summary) { 41 summary.parentNode.insertBefore(messageElement, summary); 42 } 43 else { 44 document.body.appendChild(messageElement); 45 } 46 }, 47 48 /** 49 * Ensure property value has contact information 50 */ 51 validateContact: function(test, propertyName) { 52 var result = true; 53 var value = test.properties[propertyName]; 54 var values = Array.isArray(value) ? value : [value]; 55 for (var index = 0; index < values.length; index++) { 56 value = values[index]; 57 var re = /(\S+)(\s*)<(.*)>(.*)/; 58 if (! re.test(value)) { 59 re = /(\S+)(\s+)(http[s]?:\/\/)(.*)/; 60 if (! re.test(value)) { 61 this.error('Metadata property "' + propertyName + 62 '" for test: "' + test.name + 63 '" must have name and contact information ' + 64 '("name <email>" or "name http(s)://")'); 65 result = false; 66 } 67 } 68 } 69 return result; 70 }, 71 72 /** 73 * Extract metadata from test object 74 */ 75 extractFromTest: function(test) { 76 var testMetadata = {}; 77 // filter out metadata from other properties in test 78 for (var metaIndex = 0; metaIndex < this.metadataProperties.length; 79 metaIndex++) { 80 var meta = this.metadataProperties[metaIndex]; 81 if (test.properties.hasOwnProperty(meta)) { 82 if ('author' == meta) { 83 this.validateContact(test, meta); 84 } 85 testMetadata[meta] = test.properties[meta]; 86 } 87 } 88 return testMetadata; 89 }, 90 91 /** 92 * Compare cached metadata to extracted metadata 93 */ 94 validateCache: function() { 95 for (var testName in this.currentMetadata) { 96 if (! this.cachedMetadata.hasOwnProperty(testName)) { 97 return false; 98 } 99 var testMetadata = this.currentMetadata[testName]; 100 var cachedTestMetadata = this.cachedMetadata[testName]; 101 delete this.cachedMetadata[testName]; 102 103 for (var metaIndex = 0; metaIndex < this.metadataProperties.length; 104 metaIndex++) { 105 var meta = this.metadataProperties[metaIndex]; 106 if (cachedTestMetadata.hasOwnProperty(meta) && 107 testMetadata.hasOwnProperty(meta)) { 108 if (Array.isArray(cachedTestMetadata[meta])) { 109 if (! Array.isArray(testMetadata[meta])) { 110 return false; 111 } 112 if (cachedTestMetadata[meta].length == 113 testMetadata[meta].length) { 114 for (var index = 0; 115 index < cachedTestMetadata[meta].length; 116 index++) { 117 if (cachedTestMetadata[meta][index] != 118 testMetadata[meta][index]) { 119 return false; 120 } 121 } 122 } 123 else { 124 return false; 125 } 126 } 127 else { 128 if (Array.isArray(testMetadata[meta])) { 129 return false; 130 } 131 if (cachedTestMetadata[meta] != testMetadata[meta]) { 132 return false; 133 } 134 } 135 } 136 else if (cachedTestMetadata.hasOwnProperty(meta) || 137 testMetadata.hasOwnProperty(meta)) { 138 return false; 139 } 140 } 141 } 142 for (var testName in this.cachedMetadata) { 143 return false; 144 } 145 return true; 146 }, 147 148 appendText: function(elemement, text) { 149 elemement.appendChild(document.createTextNode(text)); 150 }, 151 152 jsonifyArray: function(arrayValue, indent) { 153 var output = '['; 154 155 if (1 == arrayValue.length) { 156 output += JSON.stringify(arrayValue[0]); 157 } 158 else { 159 for (var index = 0; index < arrayValue.length; index++) { 160 if (0 < index) { 161 output += ',\n ' + indent; 162 } 163 output += JSON.stringify(arrayValue[index]); 164 } 165 } 166 output += ']'; 167 return output; 168 }, 169 170 jsonifyObject: function(objectValue, indent) { 171 var output = '{'; 172 var value; 173 174 var count = 0; 175 for (var property in objectValue) { 176 ++count; 177 if (Array.isArray(objectValue[property]) || 178 ('object' == typeof(value))) { 179 ++count; 180 } 181 } 182 if (1 == count) { 183 for (var property in objectValue) { 184 output += ' "' + property + '": ' + 185 JSON.stringify(objectValue[property]) + 186 ' '; 187 } 188 } 189 else { 190 var first = true; 191 for (var property in objectValue) { 192 if (! first) { 193 output += ','; 194 } 195 first = false; 196 output += '\n ' + indent + '"' + property + '": '; 197 value = objectValue[property]; 198 if (Array.isArray(value)) { 199 output += this.jsonifyArray(value, indent + 200 ' '.substr(0, 5 + property.length)); 201 } 202 else if ('object' == typeof(value)) { 203 output += this.jsonifyObject(value, indent + ' '); 204 } 205 else { 206 output += JSON.stringify(value); 207 } 208 } 209 if (1 < output.length) { 210 output += '\n' + indent; 211 } 212 } 213 output += '}'; 214 return output; 215 }, 216 217 /** 218 * Generate javascript source code for captured metadata 219 * Metadata is in pretty-printed JSON format 220 */ 221 generateSource: function() { 222 var source = 223 '<script id="metadata_cache">/*\n' + 224 this.jsonifyObject(this.currentMetadata, '') + '\n' + 225 '*/</script>\n'; 226 return source; 227 }, 228 229 /** 230 * Add element containing metadata source code 231 */ 232 addSourceElement: function(event) { 233 var sourceWrapper = document.createElement('div'); 234 sourceWrapper.setAttribute('id', 'metadata_source'); 235 236 var instructions = document.createElement('p'); 237 if (this.cachedMetadata) { 238 this.appendText(instructions, 239 'Replace the existing <script id="metadata_cache"> element ' + 240 'in the test\'s <head> with the following:'); 241 } 242 else { 243 this.appendText(instructions, 244 'Copy the following into the <head> element of the test ' + 245 'or the test\'s metadata sidecar file:'); 246 } 247 sourceWrapper.appendChild(instructions); 248 249 var sourceElement = document.createElement('pre'); 250 this.appendText(sourceElement, this.generateSource()); 251 252 sourceWrapper.appendChild(sourceElement); 253 254 var messageElement = document.getElementById('metadata_issue'); 255 messageElement.parentNode.insertBefore(sourceWrapper, 256 messageElement.nextSibling); 257 messageElement.parentNode.removeChild(messageElement); 258 259 (event.preventDefault) ? event.preventDefault() : 260 event.returnValue = false; 261 }, 262 263 /** 264 * Extract the metadata cache from the cache element if present 265 */ 266 getCachedMetadata: function() { 267 var cacheElement = document.getElementById('metadata_cache'); 268 269 if (cacheElement) { 270 var cacheText = cacheElement.firstChild.nodeValue; 271 var openBrace = cacheText.indexOf('{'); 272 var closeBrace = cacheText.lastIndexOf('}'); 273 if ((-1 < openBrace) && (-1 < closeBrace)) { 274 cacheText = cacheText.slice(openBrace, closeBrace + 1); 275 try { 276 this.cachedMetadata = JSON.parse(cacheText); 277 } 278 catch (exc) { 279 this.cachedMetadata = 'Invalid JSON in Cached metadata. '; 280 } 281 } 282 else { 283 this.cachedMetadata = 'Metadata not found in cache element. '; 284 } 285 } 286 }, 287 288 /** 289 * Main entry point, extract metadata from tests, compare to cached version 290 * if present. 291 * If cache not present or differs from extrated metadata, generate an error 292 */ 293 process: function(tests) { 294 for (var index = 0; index < tests.length; index++) { 295 var test = tests[index]; 296 if (this.currentMetadata.hasOwnProperty(test.name)) { 297 this.error('Duplicate test name: ' + test.name); 298 } 299 else { 300 this.currentMetadata[test.name] = this.extractFromTest(test); 301 } 302 } 303 304 this.getCachedMetadata(); 305 306 var message = null; 307 var messageClass = 'warning'; 308 var showSource = false; 309 310 if (0 === tests.length) { 311 if (this.cachedMetadata) { 312 message = 'Cached metadata present but no tests. '; 313 } 314 } 315 else if (1 === tests.length) { 316 if (this.cachedMetadata) { 317 message = 'Single test files should not have cached metadata. '; 318 } 319 else { 320 var testMetadata = this.currentMetadata[tests[0].name]; 321 for (var meta in testMetadata) { 322 if (testMetadata.hasOwnProperty(meta)) { 323 message = 'Single tests should not have metadata. ' + 324 'Move metadata to <head>. '; 325 break; 326 } 327 } 328 } 329 } 330 else { 331 if (this.cachedMetadata) { 332 messageClass = 'error'; 333 if ('string' == typeof(this.cachedMetadata)) { 334 message = this.cachedMetadata; 335 showSource = true; 336 } 337 else if (! this.validateCache()) { 338 message = 'Cached metadata out of sync. '; 339 showSource = true; 340 } 341 } 342 } 343 344 if (message) { 345 var messageElement = document.createElement('p'); 346 messageElement.setAttribute('id', 'metadata_issue'); 347 messageElement.setAttribute('class', messageClass); 348 this.appendText(messageElement, message); 349 350 if (showSource) { 351 var link = document.createElement('a'); 352 this.appendText(link, 'Click for source code.'); 353 link.setAttribute('href', '#'); 354 link.setAttribute('onclick', 355 'metadata_generator.addSourceElement(event)'); 356 messageElement.appendChild(link); 357 } 358 359 var summary = document.getElementById('summary'); 360 if (summary) { 361 summary.parentNode.insertBefore(messageElement, summary); 362 } 363 else { 364 var log = document.getElementById('log'); 365 if (log) { 366 log.appendChild(messageElement); 367 } 368 } 369 } 370 }, 371 372 setup: function() { 373 add_completion_callback( 374 function (tests, harness_status) { 375 metadata_generator.process(tests, harness_status); 376 }); 377 } 378}; 379 380var url = document.URL; 381var path = url.slice(0, url.lastIndexOf('/')); 382if (path.slice(-13).indexOf('interpolation') != -1) { 383 document.write('<script src="../../../web-animations-next.dev.js"></script>'); 384} else { 385 document.write('<script src="../../web-animations-next.dev.js"></script>'); 386} 387 388if (window.parent && parent.window.initTestHarness) { 389 parent.window.initTestHarness(window); 390} else { 391 metadata_generator.setup(); 392} 393 394/* If the parent window has a testharness_properties object, 395 * we use this to provide the test settings. This is used by the 396 * default in-browser runner to configure the timeout and the 397 * rendering of results 398 */ 399try { 400 if (window.opener && "testharness_properties" in window.opener) { 401 /* If we pass the testharness_properties object as-is here without 402 * JSON stringifying and reparsing it, IE fails & emits the message 403 * "Could not complete the operation due to error 80700019". 404 */ 405 setup(JSON.parse(JSON.stringify(window.opener.testharness_properties))); 406 } 407} catch (e) { 408} 409// vim: set expandtab shiftwidth=4 tabstop=4: 410