<!doctype html>
<meta charset=utf-8>
<title>RTCPeerConnection.prototype.setLocalDescription rollback</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.js"></script>
<script>
  'use strict';

  // Test is based on the following editor draft:
  // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html

  // The following helper functions are called from RTCPeerConnection-helper.js:
  //   assert_session_desc_similar
  //   generateAudioReceiveOnlyOffer

  /*
    4.3.2.  Interface Definition
      [Constructor(optional RTCConfiguration configuration)]
      interface RTCPeerConnection : EventTarget {
        Promise<void>                      setLocalDescription(
            RTCSessionDescriptionInit description);

        readonly attribute RTCSessionDescription? localDescription;
        readonly attribute RTCSessionDescription? currentLocalDescription;
        readonly attribute RTCSessionDescription? pendingLocalDescription;

        Promise<void>                      setRemoteDescription(
            RTCSessionDescriptionInit description);

        readonly attribute RTCSessionDescription? remoteDescription;
        readonly attribute RTCSessionDescription? currentRemoteDescription;
        readonly attribute RTCSessionDescription? pendingRemoteDescription;
        ...
      };

    4.6.2.  RTCSessionDescription Class
      dictionary RTCSessionDescriptionInit {
        required RTCSdpType type;
                 DOMString  sdp = "";
      };

    4.6.1.  RTCSdpType
      enum RTCSdpType {
        "offer",
        "pranswer",
        "answer",
        "rollback"
      };
   */

  /*
    4.3.1.6.  Set the RTCSessionSessionDescription
      2.2.2.  If description is set as a local description, then run one of the
              following steps:
        - If description is of type "rollback", then this is a rollback. Set
          connection.pendingLocalDescription to null and signaling state to stable.
   */
  promise_test(t=> {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    const states = [];
    pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState));

    return pc.createOffer()
    .then(offer => pc.setLocalDescription(offer))
    .then(() => {
      assert_equals(pc.signalingState, 'have-local-offer');
      assert_not_equals(pc.localDescription, null);
      assert_not_equals(pc.pendingLocalDescription, null);
      assert_equals(pc.currentLocalDescription, null);

      return pc.setLocalDescription({ type: 'rollback' });
    })
    .then(() => {
      assert_equals(pc.signalingState, 'stable');
      assert_equals(pc.localDescription, null);
      assert_equals(pc.pendingLocalDescription, null);
      assert_equals(pc.currentLocalDescription, null);

      assert_array_equals(states, ['have-local-offer', 'stable']);
    });
  }, 'setLocalDescription(rollback) from have-local-offer state should reset back to stable state');

  /*
    4.3.1.6.  Set the RTCSessionSessionDescription
      2.3.  If the description's type is invalid for the current signaling state of
            connection, then reject p with a newly created InvalidStateError and abort
            these steps. Note that this implies that once the answerer has performed
            setLocalDescription with his answer, this cannot be rolled back.

    [jsep]
      4.1.8.2.  Rollback
        - Rollback can only be used to cancel proposed changes;
          there is no support for rolling back from a stable state to a
          previous stable state
   */
  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return promise_rejects_dom(t, 'InvalidStateError',
      pc.setLocalDescription({ type: 'rollback' }));
  }, `setLocalDescription(rollback) from stable state should reject with InvalidStateError`);

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return generateAudioReceiveOnlyOffer(pc)
    .then(offer =>
      pc.setRemoteDescription(offer)
      .then(() => pc.createAnswer()))
    .then(answer => pc.setLocalDescription(answer))
    .then(() => {
      return promise_rejects_dom(t, 'InvalidStateError',
        pc.setLocalDescription({ type: 'rollback' }));
    });
  }, `setLocalDescription(rollback) after setting answer description should reject with InvalidStateError`);

  promise_test(async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    const offer = await generateAudioReceiveOnlyOffer(pc);
    await pc.setRemoteDescription(offer);
    await promise_rejects_dom(t, 'InvalidStateError', pc.setLocalDescription({ type: 'rollback' }));
  }, `setLocalDescription(rollback) after setting a remote offer should reject with InvalidStateError`);

  promise_test(t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());
    return pc.createOffer()
    .then(offer => pc.setLocalDescription(offer))
    .then(() => pc.setLocalDescription({
      type: 'rollback',
      sdp: '!<Invalid SDP Content>;'
    }));
  }, `setLocalDescription(rollback) should ignore invalid sdp content and succeed`);

  promise_test(async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    pc.addTransceiver('audio', { direction: 'recvonly' });
    await pc.setLocalDescription(await pc.createOffer());
    const sldPromise = pc.setLocalDescription({type: "rollback"});

    assert_equals(pc.signalingState, "have-local-offer", "signalingState should not be set synchronously after a call to sLD");

    assert_not_equals(pc.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sLD");
    assert_equals(pc.pendingLocalDescription.type, "offer");
    assert_equals(pc.pendingLocalDescription.sdp, pc.localDescription.sdp);
    assert_equals(pc.pendingRemoteDescription, null, "pendingRemoteDescription should never be set due to sLD(offer)");

    const stablePromise = new Promise(resolve => {
      pc.onsignalingstatechange = () => {
        resolve(pc.signalingState);
      }
    });
    const raceValue = await Promise.race([stablePromise, sldPromise]);
    assert_equals(raceValue, "stable", "signalingstatechange event should fire before sLD resolves");
    assert_equals(pc.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
    assert_equals(pc.pendingRemoteDescription, null, "pendingRemoteDescription should never be set due to sLD(offer)");

    await sldPromise;
  }, "setLocalDescription(rollback) should update internal state with a queued tassk, in the right order");

</script>
