tor-browser

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

error-sequence.html (13554B)


      1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4 <title>Test sequence of effects of errors
      5 </title>
      6 <script src="/resources/testharness.js"></script>
      7 <script src="/resources/testharnessreport.js"></script>
      8 <script src="/media-source/mediasource-util.js"></script>
      9 </head>
     10 <body>
     11 </body>
     12 <script>
     13 'use strict';
     14 
     15 function create_audio(t) {
     16  const audio = document.createElement('audio');
     17  audio.controls = true;
     18  audio.watcher = new EventWatcher(
     19    t, audio,
     20    [
     21      'loadstart',
     22      'waiting',
     23      'error',
     24      'ended',
     25      'loadedmetadata',
     26      'canplay',
     27      'volumechange',
     28      'playing',
     29      'pause',
     30    ]);
     31  document.body.appendChild(audio);
     32  return audio;
     33 }
     34 
     35 promise_test(async t => {
     36  const audio = create_audio(t);
     37  audio.src = '';
     38  assert_equals(audio.error, null, 'initial error attribute');
     39  // Queue a volumechange event on the media element task source.
     40  audio.volume = 0;
     41  // The dedicated media source failure steps are described as queued, but
     42  // browsers do not make state changes asynchronously.
     43  // https://github.com/whatwg/html/issues/11155
     44  audio.onvolumechange = t.step_func(() => {
     45    assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
     46                  'error code');
     47    // Queue a second volumechange.  This arrives after the error event
     48    // because the error event is queued immediately after the resource
     49    // selection algorithm synchronous steps.
     50    audio.volume = 1;
     51  });
     52  await audio.watcher.wait_for(
     53    ['volumechange', 'loadstart', 'error', 'volumechange']);
     54 }, 'empty src attribute');
     55 
     56 promise_test(async t => {
     57  const audio = create_audio(t);
     58  // src is such that "the result of encoding-parsing a URL" is failure.
     59  audio.src = 'https://#fragment';
     60  assert_equals(audio.error, null, 'initial error attribute');
     61  // Queue a volumechange event on the media element task source.
     62  audio.volume = 0;
     63  // The dedicated media source failure steps are described as queued from
     64  // parallel steps in the resource selection algorithm, but browsers do not
     65  // make state changes asynchronously, and they queue the error event
     66  // immediately after the resource selection algorithm synchronous steps.
     67  // https://github.com/whatwg/html/issues/11155
     68  audio.onvolumechange = t.step_func(() => {
     69    assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
     70                  'error code');
     71    audio.volume = 1;
     72  });
     73  await audio.watcher.wait_for(
     74    ['volumechange', 'loadstart', 'error', 'volumechange']);
     75 }, 'urlRecord failure');
     76 
     77 let resource;
     78 promise_test(async t => {
     79  resource = await MediaSourceUtil.fetchResourceOfManifest(
     80    t,
     81    '/media-source/webm/test-a-128k-44100Hz-1ch-manifest.json');
     82 }, 'fetch resource');
     83 
     84 async function create_audio_with_source_buffer(t) {
     85  const audio = create_audio(t);
     86 
     87  audio.source = new MediaSource();
     88  audio.source.watcher = new EventWatcher(t, audio.source, ['sourceopen']);
     89  audio.src = URL.createObjectURL(audio.source);
     90  await audio.watcher.wait_for('loadstart');
     91  await audio.source.watcher.wait_for('sourceopen');
     92 
     93  assert_implements_optional(MediaSource.isTypeSupported(resource.type),
     94                             `${resource.type} supported`);
     95 
     96  audio.buffer = audio.source.addSourceBuffer(resource.type);
     97  assert_equals(audio.buffer.mode, 'segments',
     98                `${resource.type} buffer.mode`);
     99  audio.buffer.watcher =
    100    new EventWatcher(t, audio.buffer, ['updateend']);
    101  return audio;
    102 }
    103 
    104 // While different browsers pass different HAVE_NOTHING subtests, the four
    105 // subtests are helpful to identify the different interactions.
    106 
    107 promise_test(async t => {
    108  const audio = await create_audio_with_source_buffer(t);
    109  assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState');
    110 
    111  // Queue a volumechange event on the media element task source to check that
    112  // the event named 'error' is fired from the same task source.
    113  audio.volume = 0;
    114  audio.source.endOfStream("decode");
    115  audio.volume = 1;
    116  await audio.watcher.wait_for(['volumechange', 'error', 'volumechange']);
    117 }, 'error event while HAVE_NOTHING');
    118 
    119 // This subtest is arranged to demonstrate that the specification does not
    120 // describe what browsers do.  Please do not adjust implementations to make
    121 // this pass as https://github.com/whatwg/html/issues/11155 proposes changing
    122 // the spec.
    123 promise_test(async t => {
    124  const audio = await create_audio_with_source_buffer(t);
    125  assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState');
    126 
    127  // Queue a volumechange event on the media element task source
    128  audio.volume = 0;
    129  audio.source.endOfStream("decode");
    130  // The dedicated media source failure steps are described as queued so state
    131  // would not change until the task runs.
    132  await audio.watcher.wait_for('volumechange');
    133  assert_equals(audio.error, null, 'error attribute');
    134  await audio.watcher.wait_for('error');
    135  assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
    136                'error code');
    137 }, 'error attribute while HAVE_NOTHING');
    138 
    139 // This subtest is arranged to demonstrate that the specification does not
    140 // describe what browsers do.  Please do not adjust implementations to make
    141 // this pass as https://github.com/whatwg/html/issues/11155 proposes changing
    142 // the spec.
    143 promise_test(async t => {
    144  const audio = await create_audio_with_source_buffer(t);
    145  assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState');
    146 
    147  const play_promise = audio.play();
    148  await audio.watcher.wait_for('waiting');
    149  assert_false(audio.paused, 'paused attribute');
    150 
    151  // 'error event while HAVE_NOTHING' checks the order of events.
    152  audio.watcher.stop_watching();
    153 
    154  // Queue a volumechange event on the media element task source to see
    155  // whether the play promise is rejected from a task on same task source.
    156  audio.volume = 0;
    157  audio.source.endOfStream("decode");
    158  audio.volume = 1;
    159  const sequence = [];
    160  const events_promise = new Promise(resolve => {
    161    audio.onvolumechange = t.step_func(() => {
    162      sequence.push('volumechange');
    163      if (sequence.filter(_ => _ == 'volumechange').length == 2) {
    164        resolve();
    165      }
    166    });
    167  });
    168  try {
    169    await play_promise;
    170    assert_unreached('promise should reject');
    171  } catch {
    172    sequence.push('rejection');
    173  }
    174  await events_promise;
    175  assert_array_equals(sequence, ['volumechange', 'rejection', 'volumechange'],
    176                      'sequence');
    177 }, 'play() promise while HAVE_NOTHING');
    178 
    179 // This subtest is arranged to demonstrate inconsistencies between
    180 // implementations.  Please do not adjust implementations to make this pass as
    181 // https://github.com/whatwg/html/issues/11155 proposes changing the spec.
    182 promise_test(async t => {
    183  const audio = await create_audio_with_source_buffer(t);
    184  assert_equals(audio.readyState, audio.HAVE_NOTHING, 'readyState');
    185 
    186  const play_promise = audio.play();
    187  await audio.watcher.wait_for('waiting');
    188  assert_false(audio.paused, 'paused attribute');
    189 
    190  // 'error event while HAVE_NOTHING' checks the order of events.
    191  audio.watcher.stop_watching();
    192 
    193  // The resource selection algorithm describes the dedicated media source
    194  // failure steps as queued and the event named "error" as fired
    195  // synchronously from those steps.
    196  // https://html.spec.whatwg.org/multipage/media.html#concept-media-load-algorithm
    197  // That is not what browsers do, but, as described, the error event would
    198  // arrive before the pending play promise rejection.
    199  audio.source.endOfStream("decode");
    200  const sequence = [];
    201  const event_promise = new Promise(resolve => {
    202    audio.onerror = t.step_func(() => {
    203      sequence.push('event');
    204      assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
    205                    'error code on event');
    206      resolve();
    207    });
    208  });
    209  try {
    210    await play_promise;
    211    assert_unreached('promise should reject');
    212  } catch {
    213    sequence.push('rejection');
    214  }
    215  assert_equals(audio.error?.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
    216                'error code on rejection');
    217  await event_promise;
    218  assert_array_equals(sequence, ['event', 'rejection'], 'sequence');
    219 }, 'play() promise after error event while HAVE_NOTHING');
    220 
    221 promise_test(async t => {
    222  const audio = await create_audio_with_source_buffer(t);
    223  // Truncate at the end of the metadata.
    224  audio.buffer.appendBuffer(
    225    resource.data.subarray(0, resource.cluster_start[0]));
    226  await Promise.all([
    227    audio.watcher.wait_for('loadedmetadata'),
    228    audio.buffer.watcher.wait_for('updateend'),
    229  ]);
    230  assert_equals(audio.readyState, audio.HAVE_METADATA, 'loadedmetadata');
    231 
    232  const play_promise1 = audio.play();
    233  await audio.watcher.wait_for('waiting');
    234  assert_false(audio.paused, 'paused attribute');
    235 
    236  let settled = 'NOT SETTLED';
    237  play_promise1.catch(_ => _).then(_ => settled = _);
    238 
    239  assert_equals(audio.error, null, 'error attribute');
    240  // Trigger "If the media data is corrupted" in the media data processing
    241  // steps list.
    242  // https://html.spec.whatwg.org/multipage/media.html#media-data-processing-steps-list
    243  audio.source.endOfStream("decode");
    244  // The error event is described as firing synchronously during endOfStream(),
    245  // but no browsers do this.  https://github.com/whatwg/html/issues/11155
    246  await audio.watcher.wait_for('error');
    247  // The error attribute should be set synchronously, but this checked late
    248  // just for Blink.
    249  assert_equals(audio.error?.code, MediaError.MEDIA_ERR_DECODE, 'error code');
    250  // The end of stream algorithm does not change duration on error
    251  // https://w3c.github.io/media-source/#dfn-end-of-stream
    252  assert_equals(audio.duration, 2.023, 'duration');
    253  // MEDIA_ERR_DECODE does not reject the pending play promise
    254  // https://github.com/whatwg/html/issues/505#issuecomment-178046408
    255  // because playback is sometimes possible after such errors,
    256  // https://github.com/whatwg/html/pull/509#issuecomment-174967812
    257  // as in the 'error after HAVE_FUTURE_DATA' subtest below.
    258  // Trigger volumechange for media element task source tasks.
    259  // Await 2 tasks to check that the play() promise is not about to be
    260  // rejected.
    261  // 2 is the number of tasks necessary to wait for the spurious promise
    262  // rejection with Blink.
    263  for (const i of Array(2).keys()) {
    264    audio.volume = i % 2;
    265    await audio.watcher.wait_for('volumechange');
    266  }
    267  assert_equals(settled, 'NOT SETTLED', 'play(() promise should not settle');
    268 
    269  // Check that the promise is rejected when appropriate.
    270  audio.pause();
    271  const sequence = [];
    272  const play_promise2 = new Promise(resolve => {
    273    audio.onpause = () => {
    274      sequence.push('pause');
    275      if (sequence.filter(_ => _ == 'pause').length == 2) {
    276        audio.onpause = null;
    277        return;
    278      }
    279      resolve(audio.play());
    280      audio.pause();
    281    }
    282  });
    283  audio.onwaiting = () => {
    284    sequence.push('waiting');
    285  }
    286  assert_true(audio.paused, 'paused attribute');
    287  await Promise.all([
    288    audio.watcher.wait_for(['pause', 'waiting', 'pause']),
    289    play_promise1.catch(() => sequence.push('promise1')),
    290    play_promise2.catch(() => sequence.push('promise2')),
    291  ]);
    292  assert_array_equals(sequence,
    293                      ['pause', 'promise1', 'waiting', 'pause', 'promise2'],
    294                      'sequence');
    295  promise_rejects_dom(
    296    t, 'AbortError', play_promise1, 'play promise rejection');
    297 }, 'error event while HAVE_METADATA');
    298 
    299 async function create_audio_with_full_resource(t) {
    300  const audio = await create_audio_with_source_buffer(t);
    301  // Just to reduce sound impacts
    302  audio.volume = 0;
    303  audio.buffer.appendBuffer(resource.data);
    304  await Promise.all([
    305    audio.watcher.wait_for(['volumechange', 'loadedmetadata', 'canplay']),
    306    audio.buffer.watcher.wait_for('updateend'),
    307  ]);
    308  assert_greater_than(audio.readyState, audio.HAVE_CURRENT_DATA,
    309                      'readyState');
    310 
    311  assert_equals(audio.error, null, 'error attribute');
    312  return audio;
    313 }
    314 
    315 promise_test(async t => {
    316  const audio = await create_audio_with_full_resource(t);
    317  audio.source.endOfStream("decode");
    318  // The error event is specified to fire synchronously during endOfStream(),
    319  // but no browsers do this.  https://github.com/whatwg/html/issues/11155
    320  await audio.watcher.wait_for('error');
    321  // The error attribute should be set synchronously, but this is checked late
    322  // just for Blink.  It is checked synchronously in the 'error attribute
    323  // after DECODE_ERROR' subtest below.
    324  assert_equals(audio.error?.code, MediaError.MEDIA_ERR_DECODE, 'error code');
    325 
    326  const sequence = [];
    327  const play_promise1 = audio.play().then(() => sequence.push('promise1'));
    328  const play_promise2 = new Promise(resolve => {
    329    audio.onplaying = () => {
    330      sequence.push('event');
    331      resolve(audio.play().then(() => sequence.push('promise2')));
    332    };
    333  });
    334  assert_false(audio.paused, 'paused attribute');
    335  await Promise.all([
    336    audio.watcher.wait_for('playing'),
    337    play_promise1,
    338    play_promise2,
    339  ]);
    340  assert_array_equals(sequence, ['event', 'promise1', 'promise2'], 'sequence');
    341 }, 'error event after HAVE_FUTURE_DATA');
    342 
    343 // This subtest could be merged into 'error event after HAVE_FUTURE_DATA' and
    344 // 'error event while HAVE_METADATA' if/when Blink conforms with the synchronous
    345 // attribute change.
    346 promise_test(async t => {
    347  const audio = await create_audio_with_full_resource(t);
    348  audio.source.endOfStream("decode");
    349  // The error attribute is set synchronously.
    350  assert_equals(audio.error?.code, MediaError.MEDIA_ERR_DECODE, 'error code');
    351 }, 'error attribute after DECODE_ERROR');
    352 </script>
    353 </html>