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