<NME> testharnessreport.js
<BEF> /* global add_completion_callback */
/* global setup */

/*
 * This file is intended for vendors to implement
 * code needed to integrate testharness.js tests with their own test systems.
 *
 * The default implementation extracts metadata from the tests and validates
 * it against the cached version that should be present in the test source
 * file. If the cache is not found or is out of sync, source code suitable for
 * caching the metadata is optionally generated.
 *
 * The cached metadata is present for extraction by test processing tools that
 * are unable to execute javascript.
 *
 * Metadata is attached to tests via the properties parameter in the test
 * constructor. See testharness.js for details.
 *
 * Typically test system integration will attach callbacks when each test has
 * run, using add_result_callback(callback(test)), or when the whole test file
 * has completed, using
 * add_completion_callback(callback(tests, harness_status)).
 *
 * For more documentation about the callback functions and the
 * parameters they are called with see testharness.js
 */

var metadata_generator = {

    currentMetadata: {},
    cachedMetadata: false,
    metadataProperties: ['help', 'assert', 'author'],

    error: function(message) {
        var messageElement = document.createElement('p');
        messageElement.setAttribute('class', 'error');
        this.appendText(messageElement, message);

        var summary = document.getElementById('summary');
        if (summary) {
            summary.parentNode.insertBefore(messageElement, summary);
        }
        else {
            document.body.appendChild(messageElement);
        }
    },

    /**
     * Ensure property value has contact information
     */
    validateContact: function(test, propertyName) {
        var result = true;
        var value = test.properties[propertyName];
        var values = Array.isArray(value) ? value : [value];
        for (var index = 0; index < values.length; index++) {
            value = values[index];
            var re = /(\S+)(\s*)<(.*)>(.*)/;
            if (! re.test(value)) {
                re = /(\S+)(\s+)(http[s]?:\/\/)(.*)/;
                if (! re.test(value)) {
                    this.error('Metadata property "' + propertyName +
                        '" for test: "' + test.name +
                        '" must have name and contact information ' +
                        '("name <email>" or "name http(s)://")');
                    result = false;
                }
            }
        }
        return result;
    },

    /**
     * Extract metadata from test object
     */
    extractFromTest: function(test) {
        var testMetadata = {};
        // filter out metadata from other properties in test
        for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
             metaIndex++) {
            var meta = this.metadataProperties[metaIndex];
            if (test.properties.hasOwnProperty(meta)) {
                if ('author' == meta) {
                    this.validateContact(test, meta);
                }
                testMetadata[meta] = test.properties[meta];
            }
        }
        return testMetadata;
    },

    /**
     * Compare cached metadata to extracted metadata
     */
    validateCache: function() {
        for (var testName in this.currentMetadata) {
            if (! this.cachedMetadata.hasOwnProperty(testName)) {
                return false;
            }
            var testMetadata = this.currentMetadata[testName];
            var cachedTestMetadata = this.cachedMetadata[testName];
            delete this.cachedMetadata[testName];

            for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
                 metaIndex++) {
                var meta = this.metadataProperties[metaIndex];
                if (cachedTestMetadata.hasOwnProperty(meta) &&
                    testMetadata.hasOwnProperty(meta)) {
                    if (Array.isArray(cachedTestMetadata[meta])) {
                      if (! Array.isArray(testMetadata[meta])) {
                          return false;
                      }
                      if (cachedTestMetadata[meta].length ==
                          testMetadata[meta].length) {
                          for (var index = 0;
                               index < cachedTestMetadata[meta].length;
                               index++) {
                              if (cachedTestMetadata[meta][index] !=
                                  testMetadata[meta][index]) {
                                  return false;
                              }
                          }
                      }
                      else {
                          return false;
                      }
                    }
                    else {
                      if (Array.isArray(testMetadata[meta])) {
                        return false;
                      }
                      if (cachedTestMetadata[meta] != testMetadata[meta]) {
                        return false;
                      }
                    }
                }
                else if (cachedTestMetadata.hasOwnProperty(meta) ||
                         testMetadata.hasOwnProperty(meta)) {
                    return false;
                }
            }
        }
        for (var testName in this.cachedMetadata) {
            return false;
        }
        return true;
    },

    appendText: function(elemement, text) {
        elemement.appendChild(document.createTextNode(text));
    },

    jsonifyArray: function(arrayValue, indent) {
        var output = '[';

        if (1 == arrayValue.length) {
            output += JSON.stringify(arrayValue[0]);
        }
        else {
            for (var index = 0; index < arrayValue.length; index++) {
                if (0 < index) {
                    output += ',\n  ' + indent;
                }
                output += JSON.stringify(arrayValue[index]);
            }
        }
        output += ']';
        return output;
    },

    jsonifyObject: function(objectValue, indent) {
        var output = '{';
        var value;

        var count = 0;
        for (var property in objectValue) {
            ++count;
            if (Array.isArray(objectValue[property]) ||
                ('object' == typeof(value))) {
                ++count;
            }
        }
        if (1 == count) {
            for (var property in objectValue) {
                output += ' "' + property + '": ' +
                    JSON.stringify(objectValue[property]) +
                    ' ';
            }
        }
        else {
            var first = true;
            for (var property in objectValue) {
                if (! first) {
                    output += ',';
                }
                first = false;
                output += '\n  ' + indent + '"' + property + '": ';
                value = objectValue[property];
                if (Array.isArray(value)) {
                    output += this.jsonifyArray(value, indent +
                        '                '.substr(0, 5 + property.length));
                }
                else if ('object' == typeof(value)) {
                    output += this.jsonifyObject(value, indent + '  ');
                }
                else {
                    output += JSON.stringify(value);
                }
            }
            if (1 < output.length) {
                output += '\n' + indent;
            }
        }
        output += '}';
        return output;
    },

    /**
     * Generate javascript source code for captured metadata
     * Metadata is in pretty-printed JSON format
     */
    generateSource: function() {
        /* "\/" is used instead of a plain forward slash so that the contents
        of testharnessreport.js can (for convenience) be copy-pasted into a
        script tag without issue. Otherwise, the HTML parser would think that
        the script ended in the middle of that string literal. */
        var source =
            '<script id="metadata_cache">/*\n' +
            this.jsonifyObject(this.currentMetadata, '') + '\n' +
            '*/<\/script>\n';
        return source;
    },

    /**
     * Add element containing metadata source code
     */
    addSourceElement: function(event) {
        var sourceWrapper = document.createElement('div');
        sourceWrapper.setAttribute('id', 'metadata_source');

        var instructions = document.createElement('p');
        if (this.cachedMetadata) {
            this.appendText(instructions,
                'Replace the existing <script id="metadata_cache"> element ' +
                'in the test\'s <head> with the following:');
        }
        else {
            this.appendText(instructions,
                'Copy the following into the <head> element of the test ' +
                'or the test\'s metadata sidecar file:');
        }
        sourceWrapper.appendChild(instructions);

        var sourceElement = document.createElement('pre');
        this.appendText(sourceElement, this.generateSource());

        sourceWrapper.appendChild(sourceElement);

        var messageElement = document.getElementById('metadata_issue');
        messageElement.parentNode.insertBefore(sourceWrapper,
                                               messageElement.nextSibling);
        messageElement.parentNode.removeChild(messageElement);

        (event.preventDefault) ? event.preventDefault() :
                                 event.returnValue = false;
    },

    /**
     * Extract the metadata cache from the cache element if present
     */
    getCachedMetadata: function() {
        var cacheElement = document.getElementById('metadata_cache');

        if (cacheElement) {
            var cacheText = cacheElement.firstChild.nodeValue;
            var openBrace = cacheText.indexOf('{');
            var closeBrace = cacheText.lastIndexOf('}');
            if ((-1 < openBrace) && (-1 < closeBrace)) {
                cacheText = cacheText.slice(openBrace, closeBrace + 1);
                try {
                    this.cachedMetadata = JSON.parse(cacheText);
                }
                catch (exc) {
                    this.cachedMetadata = 'Invalid JSON in Cached metadata. ';
                }
            }
            else {
                this.cachedMetadata = 'Metadata not found in cache element. ';
            }
        }
    },

    /**
     * Main entry point, extract metadata from tests, compare to cached version
     * if present.
     * If cache not present or differs from extrated metadata, generate an error
     */
    process: function(tests) {
        for (var index = 0; index < tests.length; index++) {
            var test = tests[index];
            if (this.currentMetadata.hasOwnProperty(test.name)) {
                this.error('Duplicate test name: ' + test.name);
            }
            else {
                this.currentMetadata[test.name] = this.extractFromTest(test);
            }
        }

        this.getCachedMetadata();

        var message = null;
        var messageClass = 'warning';
        var showSource = false;

        if (0 === tests.length) {
            if (this.cachedMetadata) {
                message = 'Cached metadata present but no tests. ';
            }
        }
        else if (1 === tests.length) {
            if (this.cachedMetadata) {
                message = 'Single test files should not have cached metadata. ';
            }
            else {
                var testMetadata = this.currentMetadata[tests[0].name];
                for (var meta in testMetadata) {
                    if (testMetadata.hasOwnProperty(meta)) {
                        message = 'Single tests should not have metadata. ' +
                                  'Move metadata to <head>. ';
                        break;
                    }
                }
            }
        }
        else {
            if (this.cachedMetadata) {
                messageClass = 'error';
                if ('string' == typeof(this.cachedMetadata)) {
                    message = this.cachedMetadata;
                    showSource = true;
                }
                else if (! this.validateCache()) {
                    message = 'Cached metadata out of sync. ';
                    showSource = true;
                }
            }
        }

        if (message) {
            var messageElement = document.createElement('p');
            messageElement.setAttribute('id', 'metadata_issue');
            messageElement.setAttribute('class', messageClass);
            this.appendText(messageElement, message);

            if (showSource) {
                var link = document.createElement('a');
                this.appendText(link, 'Click for source code.');
                link.setAttribute('href', '#');
                link.setAttribute('onclick',
                                  'metadata_generator.addSourceElement(event)');
                messageElement.appendChild(link);
            }

            var summary = document.getElementById('summary');
            if (summary) {
                summary.parentNode.insertBefore(messageElement, summary);
            }
            else {
                var log = document.getElementById('log');
                if (log) {
                    log.appendChild(messageElement);
                }
            }
        }
    },

    setup: function() {
        add_completion_callback(
            function (tests, harness_status) {
                metadata_generator.process(tests, harness_status);
                dump_test_results(tests, harness_status);
            });
    }
};

function dump_test_results(tests, status) {
    var results_element = document.createElement("script");
    results_element.type = "text/json";
    results_element.id = "__testharness__results__";
    var test_results = tests.map(function(x) {
        return {name:x.name, status:x.status, message:x.message, stack:x.stack}
    });
    var data = {test:window.location.href,
                tests:test_results,
                status: status.status,
                message: status.message,
                stack: status.stack};
    results_element.textContent = JSON.stringify(data);

    // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element'
    // is inserted at a location that results in a valid document.
    var parent = document.body
        ? document.body                 // <body> is required in XHTML documents
        : document.documentElement;     // fallback for optional <body> in HTML5, SVG, etc.

    parent.appendChild(results_element);
}

/* BEGIN WEB ANIMATIONS POLYFILL EXTRAS */
// The path /polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.
document.write('<script src="/polyfill.js"></script>');
if (window.parent && parent.window.onTestharnessLoaded) {
    parent.window.onTestharnessLoaded(window);
} else {
    Element.prototype.animate = null;
}
document.write('<script src="/base/' + config.target + '.min.js"></script>');
parent.window.onTestharnessLoaded(window);

/* END WEB ANIMATIONS POLYFILL EXTRAS */

/* If the parent window has a testharness_properties object,
 * we use this to provide the test settings. This is used by the
 * default in-browser runner to configure the timeout and the
 * rendering of results
 */
try {
    if (window.opener && "testharness_properties" in window.opener) {
        /* If we pass the testharness_properties object as-is here without
         * JSON stringifying and reparsing it, IE fails & emits the message
         * "Could not complete the operation due to error 80700019".
         */
        setup(JSON.parse(JSON.stringify(window.opener.testharness_properties)));
    }
} catch (e) {
}
// vim: set expandtab shiftwidth=4 tabstop=4:

<MSG> enableSourceMapsInKarma

<DFF> @@ -406,8 +406,8 @@ function dump_test_results(tests, status) {
 }
 
 /* BEGIN WEB ANIMATIONS POLYFILL EXTRAS */
-// The path /polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.
-document.write('<script src="/polyfill.js"></script>');
+// The path /base/polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.
+document.write('<script src="/base/polyfill.js"></script>');
 if (window.parent && parent.window.onTestharnessLoaded) {
     parent.window.onTestharnessLoaded(window);
 } else {

{'commit': 'diff --git a/Gruntfile.js b/Gruntfile.js\nindex 097828f..62a2c69 100644\n--- a/Gruntfile.js\n+++ b/Gruntfile.js\n@@ -237,14 +237,14 @@ module.exports = function(grunt) {\n         configCallback(karmaConfig);\n         karmaConfig.client.testharnessTests = require(\'./test/web-platform-tests-expectations.js\');\n         karmaConfig.client.testharnessTests.testURLList = testFiles;\n-        karmaConfig.proxies[\'/polyfill.js\'] = \'/base/\' + task.target + \'.min.js\';\n+        karmaConfig.proxies[\'/base/polyfill.js\'] = \'/base/\' + task.target + \'.dev.js\';\n         karmaConfig.files.push(\'test/karma-testharness-adapter.js\');\n         var servedFiles = [\n           \'test/web-platform-tests/resources/**\',\n           \'test/web-platform-tests/web-animations/**\',\n           \'test/resources/*\',\n           \'src/**\',\n-          \'*.js\',\n+          \'*.js*\',\n \n           // TODO(alancutter): Make a separate grunt task for serving these imported Blink tests.\n           \'test/blink/**\',\ndiff --git a/test/resources/testharnessreport.js b/test/resources/testharnessreport.js\nindex 66fe608..aec9689 100644\n--- a/test/resources/testharnessreport.js\n+++ b/test/resources/testharnessreport.js\n@@ -406,8 +406,8 @@ function dump_test_results(tests, status) {\n }\n \n /* BEGIN WEB ANIMATIONS POLYFILL EXTRAS */\n-// The path /polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.\n-document.write(\'<script src="/polyfill.js"></script>\');\n+// The path /base/polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.\n+document.write(\'<script src="/base/polyfill.js"></script>\');\n if (window.parent && parent.window.onTestharnessLoaded) {\n     parent.window.onTestharnessLoaded(window);\n } else {\n', 'commit_subject': 'enableSourceMapsInKarma', 'repo_name': 'web-animations/web-animations-js', 'committer': {'name': 'Alan Cutter', 'email': 'ac1590d290b07bb66715d89256504ee7347341ba@chromium.org', 'time_sec': '1465889121', 'tz_offset': '600', 'date': {'seconds': '1465889121'}}, 'commit_tree': 'c29cbbb4e4582b53ad390c4aec474364eb51a9cf', 'commit_message': 'enableSourceMapsInKarma\n', 'language_name': 'Shell', 'license': 'apache-2.0', 'hunks': '@@ -406,8 +406,8 @@ function dump_test_results(tests, status) {\n }\n \n /* BEGIN WEB ANIMATIONS POLYFILL EXTRAS */\n-// The path /polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.\n-document.write(\'<script src="/polyfill.js"></script>\');\n+// The path /base/polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.\n+document.write(\'<script src="/base/polyfill.js"></script>\');\n if (window.parent && parent.window.onTestharnessLoaded) {\n     parent.window.onTestharnessLoaded(window);\n } else {\n', 'addition_count': 2, 'deletion_count': 2, 'src_file': 'a/test/resources/testharnessreport.js', 'tgt_file': 'b/test/resources/testharnessreport.js', 'file_extension': '.js', 'before_file': '/* global add_completion_callback */\n/* global setup */\n\n/*\n * This file is intended for vendors to implement\n * code needed to integrate testharness.js tests with their own test systems.\n *\n * The default implementation extracts metadata from the tests and validates\n * it against the cached version that should be present in the test source\n * file. If the cache is not found or is out of sync, source code suitable for\n * caching the metadata is optionally generated.\n *\n * The cached metadata is present for extraction by test processing tools that\n * are unable to execute javascript.\n *\n * Metadata is attached to tests via the properties parameter in the test\n * constructor. See testharness.js for details.\n *\n * Typically test system integration will attach callbacks when each test has\n * run, using add_result_callback(callback(test)), or when the whole test file\n * has completed, using\n * add_completion_callback(callback(tests, harness_status)).\n *\n * For more documentation about the callback functions and the\n * parameters they are called with see testharness.js\n */\n\nvar metadata_generator = {\n\n    currentMetadata: {},\n    cachedMetadata: false,\n    metadataProperties: [\'help\', \'assert\', \'author\'],\n\n    error: function(message) {\n        var messageElement = document.createElement(\'p\');\n        messageElement.setAttribute(\'class\', \'error\');\n        this.appendText(messageElement, message);\n\n        var summary = document.getElementById(\'summary\');\n        if (summary) {\n            summary.parentNode.insertBefore(messageElement, summary);\n        }\n        else {\n            document.body.appendChild(messageElement);\n        }\n    },\n\n    /**\n     * Ensure property value has contact information\n     */\n    validateContact: function(test, propertyName) {\n        var result = true;\n        var value = test.properties[propertyName];\n        var values = Array.isArray(value) ? value : [value];\n        for (var index = 0; index < values.length; index++) {\n            value = values[index];\n            var re = /(\\S+)(\\s*)<(.*)>(.*)/;\n            if (! re.test(value)) {\n                re = /(\\S+)(\\s+)(http[s]?:\\/\\/)(.*)/;\n                if (! re.test(value)) {\n                    this.error(\'Metadata property "\' + propertyName +\n                        \'" for test: "\' + test.name +\n                        \'" must have name and contact information \' +\n                        \'("name <email>" or "name http(s)://")\');\n                    result = false;\n                }\n            }\n        }\n        return result;\n    },\n\n    /**\n     * Extract metadata from test object\n     */\n    extractFromTest: function(test) {\n        var testMetadata = {};\n        // filter out metadata from other properties in test\n        for (var metaIndex = 0; metaIndex < this.metadataProperties.length;\n             metaIndex++) {\n            var meta = this.metadataProperties[metaIndex];\n            if (test.properties.hasOwnProperty(meta)) {\n                if (\'author\' == meta) {\n                    this.validateContact(test, meta);\n                }\n                testMetadata[meta] = test.properties[meta];\n            }\n        }\n        return testMetadata;\n    },\n\n    /**\n     * Compare cached metadata to extracted metadata\n     */\n    validateCache: function() {\n        for (var testName in this.currentMetadata) {\n            if (! this.cachedMetadata.hasOwnProperty(testName)) {\n                return false;\n            }\n            var testMetadata = this.currentMetadata[testName];\n            var cachedTestMetadata = this.cachedMetadata[testName];\n            delete this.cachedMetadata[testName];\n\n            for (var metaIndex = 0; metaIndex < this.metadataProperties.length;\n                 metaIndex++) {\n                var meta = this.metadataProperties[metaIndex];\n                if (cachedTestMetadata.hasOwnProperty(meta) &&\n                    testMetadata.hasOwnProperty(meta)) {\n                    if (Array.isArray(cachedTestMetadata[meta])) {\n                      if (! Array.isArray(testMetadata[meta])) {\n                          return false;\n                      }\n                      if (cachedTestMetadata[meta].length ==\n                          testMetadata[meta].length) {\n                          for (var index = 0;\n                               index < cachedTestMetadata[meta].length;\n                               index++) {\n                              if (cachedTestMetadata[meta][index] !=\n                                  testMetadata[meta][index]) {\n                                  return false;\n                              }\n                          }\n                      }\n                      else {\n                          return false;\n                      }\n                    }\n                    else {\n                      if (Array.isArray(testMetadata[meta])) {\n                        return false;\n                      }\n                      if (cachedTestMetadata[meta] != testMetadata[meta]) {\n                        return false;\n                      }\n                    }\n                }\n                else if (cachedTestMetadata.hasOwnProperty(meta) ||\n                         testMetadata.hasOwnProperty(meta)) {\n                    return false;\n                }\n            }\n        }\n        for (var testName in this.cachedMetadata) {\n            return false;\n        }\n        return true;\n    },\n\n    appendText: function(elemement, text) {\n        elemement.appendChild(document.createTextNode(text));\n    },\n\n    jsonifyArray: function(arrayValue, indent) {\n        var output = \'[\';\n\n        if (1 == arrayValue.length) {\n            output += JSON.stringify(arrayValue[0]);\n        }\n        else {\n            for (var index = 0; index < arrayValue.length; index++) {\n                if (0 < index) {\n                    output += \',\\n  \' + indent;\n                }\n                output += JSON.stringify(arrayValue[index]);\n            }\n        }\n        output += \']\';\n        return output;\n    },\n\n    jsonifyObject: function(objectValue, indent) {\n        var output = \'{\';\n        var value;\n\n        var count = 0;\n        for (var property in objectValue) {\n            ++count;\n            if (Array.isArray(objectValue[property]) ||\n                (\'object\' == typeof(value))) {\n                ++count;\n            }\n        }\n        if (1 == count) {\n            for (var property in objectValue) {\n                output += \' "\' + property + \'": \' +\n                    JSON.stringify(objectValue[property]) +\n                    \' \';\n            }\n        }\n        else {\n            var first = true;\n            for (var property in objectValue) {\n                if (! first) {\n                    output += \',\';\n                }\n                first = false;\n                output += \'\\n  \' + indent + \'"\' + property + \'": \';\n                value = objectValue[property];\n                if (Array.isArray(value)) {\n                    output += this.jsonifyArray(value, indent +\n                        \'                \'.substr(0, 5 + property.length));\n                }\n                else if (\'object\' == typeof(value)) {\n                    output += this.jsonifyObject(value, indent + \'  \');\n                }\n                else {\n                    output += JSON.stringify(value);\n                }\n            }\n            if (1 < output.length) {\n                output += \'\\n\' + indent;\n            }\n        }\n        output += \'}\';\n        return output;\n    },\n\n    /**\n     * Generate javascript source code for captured metadata\n     * Metadata is in pretty-printed JSON format\n     */\n    generateSource: function() {\n        /* "\\/" is used instead of a plain forward slash so that the contents\n        of testharnessreport.js can (for convenience) be copy-pasted into a\n        script tag without issue. Otherwise, the HTML parser would think that\n        the script ended in the middle of that string literal. */\n        var source =\n            \'<script id="metadata_cache">/*\\n\' +\n            this.jsonifyObject(this.currentMetadata, \'\') + \'\\n\' +\n            \'*/<\\/script>\\n\';\n        return source;\n    },\n\n    /**\n     * Add element containing metadata source code\n     */\n    addSourceElement: function(event) {\n        var sourceWrapper = document.createElement(\'div\');\n        sourceWrapper.setAttribute(\'id\', \'metadata_source\');\n\n        var instructions = document.createElement(\'p\');\n        if (this.cachedMetadata) {\n            this.appendText(instructions,\n                \'Replace the existing <script id="metadata_cache"> element \' +\n                \'in the test\\\'s <head> with the following:\');\n        }\n        else {\n            this.appendText(instructions,\n                \'Copy the following into the <head> element of the test \' +\n                \'or the test\\\'s metadata sidecar file:\');\n        }\n        sourceWrapper.appendChild(instructions);\n\n        var sourceElement = document.createElement(\'pre\');\n        this.appendText(sourceElement, this.generateSource());\n\n        sourceWrapper.appendChild(sourceElement);\n\n        var messageElement = document.getElementById(\'metadata_issue\');\n        messageElement.parentNode.insertBefore(sourceWrapper,\n                                               messageElement.nextSibling);\n        messageElement.parentNode.removeChild(messageElement);\n\n        (event.preventDefault) ? event.preventDefault() :\n                                 event.returnValue = false;\n    },\n\n    /**\n     * Extract the metadata cache from the cache element if present\n     */\n    getCachedMetadata: function() {\n        var cacheElement = document.getElementById(\'metadata_cache\');\n\n        if (cacheElement) {\n            var cacheText = cacheElement.firstChild.nodeValue;\n            var openBrace = cacheText.indexOf(\'{\');\n            var closeBrace = cacheText.lastIndexOf(\'}\');\n            if ((-1 < openBrace) && (-1 < closeBrace)) {\n                cacheText = cacheText.slice(openBrace, closeBrace + 1);\n                try {\n                    this.cachedMetadata = JSON.parse(cacheText);\n                }\n                catch (exc) {\n                    this.cachedMetadata = \'Invalid JSON in Cached metadata. \';\n                }\n            }\n            else {\n                this.cachedMetadata = \'Metadata not found in cache element. \';\n            }\n        }\n    },\n\n    /**\n     * Main entry point, extract metadata from tests, compare to cached version\n     * if present.\n     * If cache not present or differs from extrated metadata, generate an error\n     */\n    process: function(tests) {\n        for (var index = 0; index < tests.length; index++) {\n            var test = tests[index];\n            if (this.currentMetadata.hasOwnProperty(test.name)) {\n                this.error(\'Duplicate test name: \' + test.name);\n            }\n            else {\n                this.currentMetadata[test.name] = this.extractFromTest(test);\n            }\n        }\n\n        this.getCachedMetadata();\n\n        var message = null;\n        var messageClass = \'warning\';\n        var showSource = false;\n\n        if (0 === tests.length) {\n            if (this.cachedMetadata) {\n                message = \'Cached metadata present but no tests. \';\n            }\n        }\n        else if (1 === tests.length) {\n            if (this.cachedMetadata) {\n                message = \'Single test files should not have cached metadata. \';\n            }\n            else {\n                var testMetadata = this.currentMetadata[tests[0].name];\n                for (var meta in testMetadata) {\n                    if (testMetadata.hasOwnProperty(meta)) {\n                        message = \'Single tests should not have metadata. \' +\n                                  \'Move metadata to <head>. \';\n                        break;\n                    }\n                }\n            }\n        }\n        else {\n            if (this.cachedMetadata) {\n                messageClass = \'error\';\n                if (\'string\' == typeof(this.cachedMetadata)) {\n                    message = this.cachedMetadata;\n                    showSource = true;\n                }\n                else if (! this.validateCache()) {\n                    message = \'Cached metadata out of sync. \';\n                    showSource = true;\n                }\n            }\n        }\n\n        if (message) {\n            var messageElement = document.createElement(\'p\');\n            messageElement.setAttribute(\'id\', \'metadata_issue\');\n            messageElement.setAttribute(\'class\', messageClass);\n            this.appendText(messageElement, message);\n\n            if (showSource) {\n                var link = document.createElement(\'a\');\n                this.appendText(link, \'Click for source code.\');\n                link.setAttribute(\'href\', \'#\');\n                link.setAttribute(\'onclick\',\n                                  \'metadata_generator.addSourceElement(event)\');\n                messageElement.appendChild(link);\n            }\n\n            var summary = document.getElementById(\'summary\');\n            if (summary) {\n                summary.parentNode.insertBefore(messageElement, summary);\n            }\n            else {\n                var log = document.getElementById(\'log\');\n                if (log) {\n                    log.appendChild(messageElement);\n                }\n            }\n        }\n    },\n\n    setup: function() {\n        add_completion_callback(\n            function (tests, harness_status) {\n                metadata_generator.process(tests, harness_status);\n                dump_test_results(tests, harness_status);\n            });\n    }\n};\n\nfunction dump_test_results(tests, status) {\n    var results_element = document.createElement("script");\n    results_element.type = "text/json";\n    results_element.id = "__testharness__results__";\n    var test_results = tests.map(function(x) {\n        return {name:x.name, status:x.status, message:x.message, stack:x.stack}\n    });\n    var data = {test:window.location.href,\n                tests:test_results,\n                status: status.status,\n                message: status.message,\n                stack: status.stack};\n    results_element.textContent = JSON.stringify(data);\n\n    // To avoid a HierarchyRequestError with XML documents, ensure that \'results_element\'\n    // is inserted at a location that results in a valid document.\n    var parent = document.body\n        ? document.body                 // <body> is required in XHTML documents\n        : document.documentElement;     // fallback for optional <body> in HTML5, SVG, etc.\n\n    parent.appendChild(results_element);\n}\n\n/* BEGIN WEB ANIMATIONS POLYFILL EXTRAS */\n// The path /polyfill.js is expected to be a proxy for the appropriate polyfill script configured in Karma.\ndocument.write(\'<script src="/polyfill.js"></script>\');\nif (window.parent && parent.window.onTestharnessLoaded) {\n    parent.window.onTestharnessLoaded(window);\n} else {\n    Element.prototype.animate = null;\n}\ndocument.write(\'<script src="/base/\' + config.target + \'.min.js"></script>\');\nparent.window.onTestharnessLoaded(window);\n\n/* END WEB ANIMATIONS POLYFILL EXTRAS */\n\n/* If the parent window has a testharness_properties object,\n * we use this to provide the test settings. This is used by the\n * default in-browser runner to configure the timeout and the\n * rendering of results\n */\ntry {\n    if (window.opener && "testharness_properties" in window.opener) {\n        /* If we pass the testharness_properties object as-is here without\n         * JSON stringifying and reparsing it, IE fails & emits the message\n         * "Could not complete the operation due to error 80700019".\n         */\n        setup(JSON.parse(JSON.stringify(window.opener.testharness_properties)));\n    }\n} catch (e) {\n}\n// vim: set expandtab shiftwidth=4 tabstop=4:\n', 'message': 'enableSourceMapsInKarma\n', 'author_name': 'Alan Cutter', 'lang': 'js'}