tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

resolve-url.js (26783B)


      1 // NOTE: this file needs to be split up rather than expanded. See ../location.sub.html for some
      2 // extracted tests. Tracked by https://github.com/web-platform-tests/wpt/issues/4934.
      3 
      4 /*
      5 * help:
      6 *   https://html.spec.whatwg.org/multipage/#the-link-element
      7 *   https://html.spec.whatwg.org/multipage/#styling
      8 *   https://html.spec.whatwg.org/multipage/#prepare-a-script
      9 *   https://html.spec.whatwg.org/multipage/#concept-media-load-algorithm
     10 *   https://html.spec.whatwg.org/multipage/#track-url
     11 *   https://html.spec.whatwg.org/multipage/#concept-form-submit
     12 *   https://html.spec.whatwg.org/multipage/#set-the-frozen-base-url
     13 *   https://dom.spec.whatwg.org/#dom-node-baseuri
     14 *   https://html.spec.whatwg.org/multipage/#the-a-element
     15 *   https://html.spec.whatwg.org/multipage/#dom-worker
     16 *   https://html.spec.whatwg.org/multipage/#dom-sharedworker
     17 *   https://html.spec.whatwg.org/multipage/#dom-eventsource
     18 *   https://html.spec.whatwg.org/multipage/#dom-xmldocument-load
     19 *   https://html.spec.whatwg.org/multipage/#dom-open
     20 *   http://url.spec.whatwg.org/#dom-url-search
     21 *   https://www.w3.org/Bugs/Public/show_bug.cgi?id=24148
     22 *   https://xhr.spec.whatwg.org/#the-open()-method
     23 *   https://html.spec.whatwg.org/multipage/#set-up-a-worker-script-settings-object
     24 *   https://html.spec.whatwg.org/multipage/#dom-workerglobalscope-importscripts
     25 *   https://html.spec.whatwg.org/multipage/#parse-a-websocket-url's-components
     26 *   https://html.spec.whatwg.org/multipage/#dom-websocket-url
     27 *   https://www.w3.org/Bugs/Public/show_bug.cgi?id=23968
     28 *   http://dev.w3.org/csswg/cssom/#requirements-on-user-agents-implementing-the-xml-stylesheet-processing-instruction
     29 *   http://url.spec.whatwg.org/#dom-url
     30 */
     31 setup({explicit_done:true});
     32 onload = function() {
     33  var encoding = '{{GET[encoding]}}';
     34  var input_url = 'resources/resource.py?q=\u00E5&encoding=' + encoding + '&type=';
     35  ('html css js worker sharedworker worker_importScripts sharedworker_importScripts worker_worker worker_sharedworker sharedworker_worker '+
     36   'sharedworker_sharedworker eventstream png svg xmlstylesheet_css video webvtt').split(' ').forEach(function(str) {
     37    window['input_url_'+str] = input_url + str;
     38  });
     39  var blank = 'resources/blank.py?encoding=' + encoding;
     40  var stash_put = 'resources/stash.py?q=\u00E5&action=put&id=';
     41  var stash_take = 'resources/stash.py?action=take&id=';
     42  var expected_obj = {
     43    'utf-8':'%C3%A5',
     44    'utf-16be':'%C3%A5',
     45    'utf-16le':'%C3%A5',
     46    'windows-1252':'%E5',
     47    'windows-1251':'%26%23229%3B'
     48  };
     49  var expected_current = expected_obj[encoding];
     50  var expected_utf8 = expected_obj['utf-8'];
     51 
     52  function msg(expected, got) {
     53    return 'expected substring '+expected+' got '+got;
     54  }
     55 
     56  function poll_for_stash(test_obj, uuid, expected) {
     57    var start = new Date();
     58    var poll = test_obj.step_func(function () {
     59      var xhr = new XMLHttpRequest();
     60      xhr.open('GET', stash_take + uuid);
     61      xhr.onload = test_obj.step_func(function(e) {
     62        if (xhr.response == "") {
     63          if (new Date() - start > 10000) {
     64            // If we set the status to TIMEOUT here we avoid a race between the
     65            // page and the test timing out
     66            test_obj.force_timeout();
     67          }
     68          test_obj.step_timeout(poll, 200);
     69        } else {
     70          assert_equals(xhr.response, expected);
     71          test_obj.done();
     72        }
     73      });
     74      xhr.send();
     75    })
     76    test_obj.step_timeout(poll, 200);
     77  }
     78 
     79  // loading html (or actually svg to support <embed>)
     80  function test_load_nested_browsing_context(tag, attr, spec_url) {
     81    subsetTestByKey('nested-browsing', async_test, function() {
     82      var id = 'test_load_nested_browsing_context_'+tag;
     83      var elm = document.createElement(tag);
     84      elm.setAttribute(attr, input_url_svg);
     85      elm.name = id;
     86      document.body.appendChild(elm);
     87      this.add_cleanup(function() {
     88        document.body.removeChild(elm);
     89      });
     90      elm.onload = this.step_func_done(function() {
     91        assert_equals(window[id].document.documentElement.textContent, expected_current);
     92      });
     93 
     94    }, 'load nested browsing context <'+tag+' '+attr+'>');
     95  }
     96 
     97  spec_url_load_nested_browsing_context = {
     98    frame:'https://html.spec.whatwg.org/multipage/#process-the-frame-attributes',
     99    iframe:'https://html.spec.whatwg.org/multipage/#process-the-iframe-attributes',
    100    object:'https://html.spec.whatwg.org/multipage/#the-object-element',
    101    embed:'https://html.spec.whatwg.org/multipage/#the-embed-element-setup-steps'
    102  };
    103 
    104  'frame src, iframe src, object data, embed src'.split(', ').forEach(function(str) {
    105    var arr = str.split(' ');
    106    test_load_nested_browsing_context(arr[0], arr[1], spec_url_load_nested_browsing_context[arr[0]]);
    107  });
    108 
    109  // loading css with <link>
    110  subsetTestByKey('loading', async_test, function() {
    111    var elm = document.createElement('link');
    112    elm.href = input_url_css;
    113    elm.rel = 'stylesheet';
    114    document.head.appendChild(elm);
    115    this.add_cleanup(function() {
    116      document.head.removeChild(elm);
    117    });
    118    elm.onload = this.step_func_done(function() {
    119      var got = elm.sheet.href;
    120      assert_true(elm.sheet.href.indexOf(expected_current) > -1, 'sheet.href ' + msg(expected_current, got));
    121      assert_equals(elm.sheet.cssRules[0].style.content, '"'+expected_current+'"', 'sheet.cssRules[0].style.content');
    122    });
    123  }, 'loading css <link>');
    124 
    125  // loading js
    126  subsetTestByKey('loading-css', async_test, function() {
    127    var elm = document.createElement('script');
    128    elm.src = input_url_js + '&var=test_load_js_got';
    129    document.head.appendChild(elm); // no cleanup
    130    elm.onload = this.step_func_done(function() {
    131      assert_equals(window.test_load_js_got, expected_current);
    132    });
    133  }, 'loading js <script>');
    134 
    135  // loading image
    136  function test_load_image(tag, attr, spec_url) {
    137    subsetTestByKey('loading', async_test, function() {
    138      var elm = document.createElement(tag);
    139      if (tag == 'input') {
    140        elm.type = 'image';
    141      }
    142      elm.setAttribute(attr, input_url_png);
    143      document.body.appendChild(elm);
    144      this.add_cleanup(function() {
    145        document.body.removeChild(elm);
    146      });
    147      elm.onload = this.step_func_done(function() {
    148        var got = elm.offsetWidth;
    149        assert_equals(got, query_to_image_width[expected_current], msg(expected_current, image_width_to_query[got]));
    150      });
    151      // <video poster> doesn't notify when the image is loaded so we need to poll :-(
    152      var interval;
    153      var check_video_width = function() {
    154        var width = elm.offsetWidth;
    155        if (width != 300 && width != 0) {
    156          clearInterval(interval);
    157          elm.onload();
    158        }
    159      }
    160      if (tag == 'video') {
    161        interval = setInterval(check_video_width, 10);
    162      }
    163    }, 'loading image <'+tag+' '+attr+'>');
    164  }
    165 
    166  var query_to_image_width = {
    167    '%E5':1,
    168    '%26%23229%3B':1,
    169    '%C3%A5':2,
    170    '%3F':16,
    171    'unknown query':256,
    172    'default intrinsic width':300
    173  };
    174 
    175  var image_width_to_query = {};
    176  (function() {
    177    for (var x in query_to_image_width) {
    178      image_width_to_query[query_to_image_width[x]] = x;
    179    }
    180  })();
    181 
    182  var spec_url_load_image = {
    183    img:'https://html.spec.whatwg.org/multipage/#update-the-image-data',
    184    embed:'https://html.spec.whatwg.org/multipage/#the-embed-element-setup-steps',
    185    object:'https://html.spec.whatwg.org/multipage/#the-object-element',
    186    input:'https://html.spec.whatwg.org/multipage/#image-button-state-(type=image)',
    187    video:'https://html.spec.whatwg.org/multipage/#poster-frame'
    188  };
    189 
    190  'img src, embed src, object data, input src, video poster'.split(', ').forEach(function(str) {
    191    var arr = str.split(' ');
    192    test_load_image(arr[0], arr[1], spec_url_load_image[arr[0]]);
    193  });
    194 
    195  // XXX test <img srcset> or its successor
    196 
    197  // loading video
    198  function test_load_video(tag, use_source_element) {
    199    subsetTestByKey('loading', async_test, function() {
    200      var elm = document.createElement(tag);
    201      var video_ext = '';
    202      if (elm.canPlayType('video/webm; codecs="vp9,opus"')) {
    203        video_ext = 'webm';
    204      } else if (elm.canPlayType('video/mp4; codecs="avc1.42E01E,mp4a.40.2"')) {
    205        video_ext = 'mp4';
    206      }
    207      assert_not_equals(video_ext, '', 'no supported video format');
    208      var source;
    209      if (use_source_element) {
    210        source = document.createElement('source');
    211        elm.appendChild(source);
    212      } else {
    213        source = elm;
    214      }
    215      source.src = input_url_video + '&ext=' + video_ext;
    216      elm.preload = 'auto';
    217      elm.load();
    218      this.add_cleanup(function() {
    219        elm.removeAttribute('src');
    220        if (elm.firstChild) {
    221          elm.removeChild(elm.firstChild);
    222        }
    223        elm.load();
    224      });
    225      elm.onloadedmetadata = this.step_func_done(function() {
    226        var got = Math.round(elm.duration);
    227        assert_equals(got, query_to_video_duration[expected_current], msg(expected_current, video_duration_to_query[got]));
    228      });
    229    }, 'loading video <'+tag+'>' + (use_source_element ? '<source>' : ''));
    230  }
    231 
    232  var query_to_video_duration = {
    233    '%E5':3,
    234    '%26%23229%3B':3,
    235    '%C3%A5':5,
    236    '%3F':30,
    237    'unknown query':300,
    238    'Infinity':Infinity,
    239    'NaN':NaN
    240  };
    241 
    242  var video_duration_to_query = {};
    243  (function() {
    244    for (var x in query_to_video_duration) {
    245      video_duration_to_query[query_to_video_duration[x]] = x;
    246    }
    247  })();
    248 
    249  'video, audio'.split(', ').forEach(function(str) {
    250    test_load_video(str);
    251    test_load_video(str, true);
    252  });
    253 
    254  // loading webvtt
    255  subsetTestByKey('loading', async_test, function() {
    256    var video = document.createElement('video');
    257    var track = document.createElement('track');
    258    video.appendChild(track);
    259    track.src = input_url_webvtt;
    260    track.track.mode = 'showing';
    261    track.onload = this.step_func_done(function() {
    262      var got = track.track.cues[0].text;
    263      assert_equals(got, expected_current);
    264    });
    265  }, 'loading webvtt <track>');
    266 
    267  // XXX downloading seems hard to automate
    268  // <a href download>
    269  // <area href download>
    270 
    271  // submit forms
    272  function test_submit_form(tag, attr) {
    273    subsetTestByKey('submit', async_test, function(){
    274      var elm = document.createElement(tag);
    275      elm.setAttribute(attr, input_url_html);
    276      var form;
    277      var button;
    278      if (tag == 'form') {
    279        form = elm;
    280        button = document.createElement('button');
    281      } else {
    282        form = document.createElement('form');
    283        button = elm;
    284      }
    285      form.method = 'post';
    286      form.appendChild(button);
    287      var iframe = document.createElement('iframe');
    288      var id = 'test_submit_form_' + tag;
    289      iframe.name = id;
    290      form.target = id;
    291      button.type = 'submit';
    292      document.body.appendChild(form);
    293      document.body.appendChild(iframe);
    294      this.add_cleanup(function() {
    295        document.body.removeChild(form);
    296        document.body.removeChild(iframe);
    297      });
    298      button.click();
    299      iframe.onload = this.step_func_done(function() {
    300        var got = iframe.contentDocument.body.textContent;
    301        if (got == '') {
    302          return;
    303        }
    304        assert_equals(got, expected_current);
    305      });
    306    }, 'submit form <'+tag+' '+attr+'>');
    307  }
    308 
    309  'form action, input formaction, button formaction'.split(', ').forEach(function(str) {
    310    var arr = str.split(' ');
    311    test_submit_form(arr[0], arr[1]);
    312  });
    313 
    314  // <base href>
    315  subsetTestByKey('base-href', async_test, function() {
    316    var iframe = document.createElement('iframe');
    317    iframe.src = blank;
    318    document.body.appendChild(iframe);
    319    this.add_cleanup(function() {
    320      document.body.removeChild(iframe);
    321    });
    322    iframe.onload = this.step_func_done(function() {
    323      var doc = iframe.contentDocument;
    324      doc.write('<!doctype html><base href="'+input_url+'"><a href></a>');
    325      doc.close();
    326      var got_baseURI = doc.baseURI;
    327      assert_true(got_baseURI.indexOf(expected_current) > -1, msg(expected_current, got_baseURI), 'doc.baseURI');
    328      var got_a_href = doc.links[0].href;
    329      assert_true(got_a_href.indexOf(expected_current) > -1, msg(expected_current, got_a_href), 'a.href');
    330    });
    331  }, '<base href>');
    332 
    333  // XXX drag and drop (<a href> or <img src>) seems hard to automate
    334 
    335  // Worker()
    336  subsetTestByKey('workers', async_test, function() {
    337    var worker = new Worker(input_url_worker);
    338    worker.onmessage = this.step_func_done(function(e) {
    339      assert_equals(e.data, expected_current);
    340    });
    341  }, 'Worker constructor');
    342 
    343  // SharedWorker()
    344  subsetTestByKey('workers', async_test, function() {
    345    var worker = new SharedWorker(input_url_sharedworker);
    346    worker.port.onmessage = this.step_func_done(function(e) {
    347      assert_equals(e.data, expected_current);
    348    });
    349  }, 'SharedWorker constructor');
    350 
    351  // EventSource()
    352  subsetTestByKey('eventsource', async_test, function() {
    353    var source = new EventSource(input_url_eventstream);
    354    this.add_cleanup(function() {
    355      source.close();
    356    });
    357    source.onmessage = this.step_func_done(function(e) {
    358      assert_equals(e.data, expected_current);
    359    });
    360  }, 'EventSource constructor');
    361 
    362  // EventSource#url
    363  subsetTestByKey('eventsource', test, function() {
    364    var source = new EventSource(input_url_eventstream);
    365    source.close();
    366    var got = source.url;
    367    assert_true(source.url.indexOf(expected_current) > -1, msg(expected_current, got));
    368  }, 'EventSource#url');
    369 
    370  // window.open
    371  subsetTestByKey('window-open', async_test, function() {
    372    var id = 'test_window_open';
    373    var iframe = document.createElement('iframe');
    374    iframe.name = id;
    375    document.body.appendChild(iframe);
    376    this.add_cleanup(function() {
    377      document.body.removeChild(iframe);
    378    });
    379    window.open(input_url_html, id);
    380    iframe.onload = this.step_func(function() {
    381      var got = iframe.contentDocument.body.textContent;
    382      if (got != "") {
    383        assert_equals(got, expected_current);
    384        this.done();
    385      }
    386    });
    387  }, 'window.open()');
    388 
    389  // a.search, area.search
    390  function test_hyperlink_search(tag) {
    391    subsetTestByKey('hyperlink-search', test, function() {
    392      var elm = document.createElement(tag);
    393      var input_arr = input_url_html.split('?');
    394      elm.href = input_arr[0];
    395      elm.search = '?' + input_arr[1];
    396      var got_href = elm.getAttribute('href');
    397      assert_true(got_href.indexOf(expected_utf8) > -1, 'href content attribute ' + msg(expected_utf8, got_href));
    398      var got_search = elm.search;
    399      assert_true(got_search.indexOf(expected_utf8) > -1, 'getting .search '+msg(expected_utf8, got_search));
    400    }, '<'+tag+'>.search');
    401  }
    402  'a, area'.split(', ').forEach(function(str) {
    403    test_hyperlink_search(str);
    404  });
    405 
    406  // history.pushState
    407  // history.replaceState
    408  function test_history(prop) {
    409    subsetTestByKey('history', async_test, function() {
    410      var url = input_url_html.replace('resources/', '');
    411      var iframe = document.createElement('iframe');
    412      iframe.src = blank;
    413      document.body.appendChild(iframe);
    414      this.add_cleanup(function() {
    415        document.body.removeChild(iframe);
    416      });
    417      iframe.onload = this.step_func_done(function() {
    418        // this should resolve against the iframe's URL
    419        // "Parse url, relative to the relevant settings object of history."
    420        // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push%2Freplace-state-steps
    421        iframe.contentWindow.history[prop](null, null, url);
    422        var got = iframe.contentWindow.location.href;
    423        assert_true(got.indexOf(expected_current) > -1, msg(expected_current, got));
    424        assert_not_equals(got.indexOf('/resources/'), -1, 'url was resolved against the test\'s URL');
    425      });
    426    }, 'history.'+prop);
    427  }
    428 
    429  'pushState, replaceState'.split(', ').forEach(function(str) {
    430    test_history(str);
    431  });
    432 
    433  // SVG
    434  var ns = {svg:'http://www.w3.org/2000/svg', xlink:'http://www.w3.org/1999/xlink'};
    435  // a
    436  subsetTestByKey('svg', async_test, function() {
    437    SVGAElement; // check support
    438    var iframe = document.createElement('iframe');
    439    var id = 'test_svg_a';
    440    iframe.name = id;
    441    var svg = document.createElementNS(ns.svg, 'svg');
    442    var a = document.createElementNS(ns.svg, 'a');
    443    a.setAttributeNS(ns.xlink, 'xlink:href', input_url_html);
    444    a.setAttribute('target', id);
    445    var span = document.createElement('span');
    446    a.appendChild(span);
    447    svg.appendChild(a);
    448    document.body.appendChild(iframe);
    449    document.body.appendChild(svg);
    450    this.add_cleanup(function() {
    451      document.body.removeChild(iframe);
    452      document.body.removeChild(svg);
    453    });
    454    span.click();
    455    iframe.onload = this.step_func_done(function() {
    456      var got = iframe.contentDocument.body.textContent;
    457      if (got != '') {
    458        assert_equals(got, expected_current);
    459      }
    460    });
    461  }, 'SVG <a>');
    462 
    463  // feImage, image, use
    464  function test_svg(func, tag) {
    465    subsetTestByKey('svg', async_test, function() {
    466      var uuid = token();
    467      var id = 'test_svg_'+tag;
    468      var svg = document.createElementNS(ns.svg, 'svg');
    469      var parent = func(svg, id);
    470      var elm = document.createElementNS(ns.svg, tag);
    471      elm.setAttributeNS(ns.xlink, 'xlink:href', stash_put + uuid + '#foo');
    472      parent.appendChild(elm);
    473      document.body.appendChild(svg);
    474      this.add_cleanup(function() {
    475        document.body.removeChild(svg);
    476      });
    477      poll_for_stash(this, uuid, expected_current);
    478    }, 'SVG <' + tag + '>');
    479  }
    480 
    481  [[function(svg, id) {
    482      SVGFEImageElement; // check support
    483      var filter = document.createElementNS(ns.svg, 'filter');
    484      filter.setAttribute('id', id);
    485      svg.appendChild(filter);
    486      var rect = document.createElementNS(ns.svg, 'rect');
    487      rect.setAttribute('filter', 'url(#'+id+')');
    488      svg.appendChild(rect);
    489      return filter;
    490    }, 'feImage'],
    491   [function(svg, id) { SVGImageElement; return svg; }, 'image'],
    492   [function(svg, id) { SVGUseElement; return svg; }, 'use']].forEach(function(arr) {
    493    test_svg(arr[0], arr[1]);
    494  });
    495 
    496  // UTF-8:
    497  // XHR
    498  subsetTestByKey('xhr', async_test, function() {
    499    var xhr = new XMLHttpRequest();
    500    xhr.open('GET', input_url_html);
    501    xhr.onload = this.step_func_done(function() {
    502      assert_equals(xhr.response, expected_current);
    503    });
    504    xhr.send();
    505  }, 'XMLHttpRequest#open()');
    506 
    507  // in a worker
    508  subsetTestByKey('workers', async_test, function() {
    509    var worker = new Worker(input_url_worker_importScripts);
    510    worker.onmessage = this.step_func_done(function(e) {
    511      assert_equals(e.data, expected_utf8);
    512    });
    513  }, 'importScripts() in a dedicated worker');
    514 
    515  subsetTestByKey('workers', async_test, function() {
    516    var worker = new Worker(input_url_worker_worker);
    517    worker.onmessage = this.step_func_done(function(e) {
    518      assert_equals(e.data, expected_utf8);
    519    });
    520  }, 'Worker() in a dedicated worker');
    521 
    522  subsetTestByKey('workers', async_test, function() {
    523    var worker = new SharedWorker(input_url_sharedworker_importScripts);
    524    worker.port.onmessage = this.step_func_done(function(e) {
    525      assert_equals(e.data, expected_utf8);
    526    });
    527  }, 'importScripts() in a shared worker');
    528 
    529  subsetTestByKey('workers', async_test, function() {
    530    var worker = new SharedWorker(input_url_sharedworker_worker);
    531    worker.port.onmessage = this.step_func_done(function(e) {
    532      assert_equals(e.data, expected_utf8);
    533    });
    534  }, 'Worker() in a shared worker');
    535 
    536  // WebSocket()
    537  subsetTestByKey('websocket', async_test, function() {
    538    var ws = new WebSocket('ws://{{host}}:{{ports[ws][0]}}/echo-query?\u00E5');
    539    this.add_cleanup(function() {
    540      ws.close();
    541    });
    542    ws.onmessage = this.step_func_done(function(e) {
    543      assert_equals(e.data, expected_utf8);
    544    });
    545  }, 'WebSocket constructor');
    546 
    547  // WebSocket#url
    548  subsetTestByKey('websocket', test, function() {
    549    var ws = new WebSocket('ws://{{host}}:{{ports[ws][0]}}/echo-query?\u00E5');
    550    ws.close();
    551    var got = ws.url;
    552    assert_true(ws.url.indexOf(expected_utf8) > -1, msg(expected_utf8, got));
    553  }, 'WebSocket#url');
    554 
    555  // CSS
    556  function test_css(tmpl, expected_cssom, encoding, use_style_element) {
    557    var desc = ['CSS', (use_style_element ? '<style>' : '<link> (' + encoding + ')'),  tmpl].join(' ');
    558    subsetTestByKey('css', async_test, function(){
    559      css_is_supported(tmpl, expected_cssom, this);
    560      var uuid = token();
    561      var id = 'test_css_' + uuid;
    562      var url = 'url(stash.py?q=%s&action=put&id=' + uuid + ')';
    563      tmpl = tmpl.replace(/<id>/g, id).replace(/<url>/g, url);
    564      var link;
    565      if (use_style_element) {
    566        link = document.createElement('style');
    567        link.textContent = tmpl.replace(/%s/g, '\u00E5').replace(/stash\.py/g, 'resources/stash.py');
    568      } else {
    569        link = document.createElement('link');
    570        link.rel = 'stylesheet';
    571        link.href = 'resources/css-tmpl.py?encoding='+encoding+'&tmpl='+encodeURIComponent(tmpl);
    572      }
    573      var div = document.createElement('div');
    574      div.id = id;
    575      div.textContent='x';
    576      document.head.appendChild(link);
    577      document.body.appendChild(div);
    578      this.add_cleanup(function() {
    579        document.head.removeChild(link);
    580        document.body.removeChild(div);
    581      });
    582      poll_for_stash(this, uuid, expected_utf8);
    583    }, desc);
    584  }
    585 
    586  // fail fast if the input doesn't parse into the expected cssom
    587  function css_is_supported(tmpl, expected_cssom, test_obj) {
    588    if (expected_cssom === null) {
    589      return;
    590    }
    591    var style = document.createElement('style');
    592    style.textContent = tmpl.replace(/<id>/g, 'x').replace(/<url>/g, 'url(data:,)');
    593    document.head.appendChild(style);
    594    test_obj.add_cleanup(function() {
    595      document.head.removeChild(style);
    596    });
    597    assert_equals(style.sheet.cssRules.length, expected_cssom.length, 'number of style rules');
    598    for (var i = 0; i < expected_cssom.length; ++i) {
    599      if (expected_cssom[i] === null) {
    600        continue;
    601      }
    602      assert_equals(style.sheet.cssRules[i].style.length, expected_cssom[i], 'number of declarations in style rule #'+i);
    603    }
    604  }
    605 
    606  [['#<id> { background-image:<url> }', [1] ],
    607   ['#<id> { border-image-source:<url> }', [1] ],
    608   ['#<id>::before { content:<url> }', [1] ],
    609   ['@font-face { font-family:<id>; src:<url> } #<id> { font-family:<id> }', [null, 1] ],
    610   ['#<id> { display:list-item; list-style-image:<url> }', [2] ],
    611   ['@import <url>;', null ],
    612   // XXX maybe cursor isn't suitable for automation here if browsers delay fetching it
    613   ['#<id> { cursor:<url>, pointer }', [1] ]].forEach(function(arr) {
    614    var input = arr[0];
    615    var expected_cssom = arr[1];
    616    var other_encoding = encoding == 'utf-8' ? 'windows-1252' : 'utf-8';
    617    test_css(input, expected_cssom, encoding);
    618    test_css(input, expected_cssom, other_encoding);
    619    test_css(input, expected_cssom, null, true);
    620   });
    621 
    622  // XXX maybe test if they become relevant:
    623  // binding (obsolete?)
    624  // aural: cue-after, cue-before, play-during (not implemented?)
    625  // hyphenate-resource (not implemented?)
    626  // image() (not implemented?)
    627 
    628  // <?xml-stylesheet?>
    629  subsetTestByKey('xml', async_test, function() {
    630    var iframe = document.createElement('iframe');
    631    iframe.src = input_url_xmlstylesheet_css;
    632    document.body.appendChild(iframe);
    633    this.add_cleanup(function() {
    634      document.body.removeChild(iframe);
    635    });
    636    iframe.onload = this.step_func_done(function() {
    637      assert_equals(iframe.contentDocument.firstChild.sheet.cssRules[0].style.content, '"' + expected_utf8 + '"');
    638    });
    639  }, '<?xml-stylesheet?> (CSS)');
    640 
    641  // new URL()
    642  subsetTestByKey('url', test, function() {
    643    var url = new URL('http://example.org/'+input_url);
    644    var expected = expected_utf8;
    645    assert_true(url.href.indexOf(expected) > -1, 'url.href '+msg(expected, url.href));
    646    assert_true(url.search.indexOf(expected) > -1, 'url.search '+msg(expected, url.search));
    647  }, 'URL constructor, url');
    648 
    649  subsetTestByKey('url', test, function() {
    650    var url = new URL('', 'http://example.org/'+input_url);
    651    var expected = expected_utf8;
    652    assert_true(url.href.indexOf(expected) > -1, 'url.href '+msg(expected, url.href));
    653    assert_true(url.search.indexOf(expected) > -1, 'url.search '+msg(expected, url.search));
    654  }, 'URL constructor, base');
    655 
    656  // Test different schemes
    657  function test_scheme(url, utf8) {
    658    subsetTestByKey('scheme', test, function() {
    659      var a = document.createElement('a');
    660      a.setAttribute('href', url);
    661      var got = a.href;
    662      var expected = utf8 ? expected_utf8 : expected_current;
    663      assert_true(got.indexOf(expected) != -1, msg(expected, got));
    664    }, 'Scheme ' + url.split(':')[0] + ' (getting <a>.href)');
    665  }
    666 
    667  var test_scheme_urls = ['ftp://example.invalid/?x=\u00E5',
    668                          'file:///?x=\u00E5',
    669                          'http://example.invalid/?x=\u00E5',
    670                          'https://example.invalid/?x=\u00E5',
    671                         ];
    672 
    673  var test_scheme_urls_utf8 = ['ws://example.invalid/?x=\u00E5',
    674                               'wss://example.invalid/?x=\u00E5',
    675                               'gopher://example.invalid/?x=\u00E5',
    676                               'mailto:example@invalid?x=\u00E5',
    677                               'data:text/plain;charset='+encoding+',?x=\u00E5',
    678                               'javascript:"?x=\u00E5"',
    679                               'ftps://example.invalid/?x=\u00E5',
    680                               'httpbogus://example.invalid/?x=\u00E5',
    681                               'bitcoin:foo?x=\u00E5',
    682                               'geo:foo?x=\u00E5',
    683                               'im:foo?x=\u00E5',
    684                               'irc:foo?x=\u00E5',
    685                               'ircs:foo?x=\u00E5',
    686                               'magnet:foo?x=\u00E5',
    687                               'mms:foo?x=\u00E5',
    688                               'news:foo?x=\u00E5',
    689                               'nntp:foo?x=\u00E5',
    690                               'sip:foo?x=\u00E5',
    691                               'sms:foo?x=\u00E5',
    692                               'smsto:foo?x=\u00E5',
    693                               'ssh:foo?x=\u00E5',
    694                               'tel:foo?x=\u00E5',
    695                               'urn:foo?x=\u00E5',
    696                               'webcal:foo?x=\u00E5',
    697                               'wtai:foo?x=\u00E5',
    698                               'xmpp:foo?x=\u00E5',
    699                               'web+http:foo?x=\u00E5',
    700                              ];
    701 
    702  test_scheme_urls.forEach(function(url) {
    703    test_scheme(url);
    704  });
    705 
    706  test_scheme_urls_utf8.forEach(function(url) {
    707    test_scheme(url, true);
    708  });
    709 
    710  done();
    711 };