<!doctype html>
<html>
    <head>
        <title>history.pushState tests</title>
        <script type="text/javascript" src="/resources/testharness.js"></script>
        <script type="text/javascript" src="/resources/testharnessreport.js"></script>
        <script type="text/javascript">
//does not test for firing of popstate onload, because this was dropped from the specification on 25 March 2011
//covers history.state after load, in accordance with the specification draft from 25 March 2011
//history.state before load is tested in 006 and 007
//does not test for structured cloning of FileList, File or Blob interfaces, as these require manual file selection

//**This test assumes that assignments to location.hash will be synchronous - this is how all browsers implement it.
//The spec (as of 25 March 2011) disagrees.**//

var histlength, atstep = 0, lasttimer;
setup({explicit_done:true}); //tests should take under 6 seconds + execution time

window.onload = function () {
    if( location.protocol == 'file:' ) {
        document.getElementsByTagName('p')[0].innerHTML = 'ERROR: This test cannot be run from file: (URL resolving will not work). It must be loaded over HTTP.';
        return;
    } else if( location.protocol == 'https:' ) {
        document.getElementsByTagName('p')[0].innerHTML += '<br>WARNING: Browsers may intentionally fail to update history.length when pages are loaded over HTTPS, as a privacy restriction. If possible, load this page over HTTP.';
    }
    //use a timeout, because some browsers intentionally do not add history entries for URL changes in the onload thread
    setTimeout(testinit,100);
};
function testinit() {
    atstep = 1;
    histlength = history.length;
    iframe = document.getElementsByTagName('iframe')[0].src = 'blank2.html';
    //reportload will now be called by the onload handler for the iframe
}
function reportload() {
    var iframe = document.getElementsByTagName('iframe')[0], hashchng = false;
    var canvassup = false, cloneobj;

    function tests1() {
        //Firefox may fail when reloading, because it recovers iframe state, and therefore does not see the need to alter history length
        test(function () { assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' ); }, 'history.length should update when loading pages in an iframe');
        histlength = history.length;
        iframe.contentWindow.location.hash = 'test'; //should be synchronous **SEE COMMENT AT TOP OF FILE
        test(function () {
            assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' );
        }, 'history.length should update when setting location.hash');
        test(function () { assert_true( !!history.pushState, 'critical test; ignore any failures after this' ); }, 'history.pushState must exist'); //assert_own_property does not allow prototype inheritance
        test(function () { assert_true( !!iframe.contentWindow.history.pushState, 'critical test; ignore any failures after this' ); }, 'history.pushState must exist within iframes');
        test(function () {
            assert_equals( iframe.contentWindow.history.state, null );
        }, 'initial history.state should be null');
        test(function () {
            histlength = history.length;
            iframe.contentWindow.history.pushState('','');
            assert_equals( history.length, histlength + 1 );
        }, 'history.length should update when pushing a state');
        test(function () {
            assert_equals( iframe.contentWindow.history.state, '' );
        }, 'history.state should update after a state is pushed');
        histlength = history.length;
        iframe.contentWindow.addEventListener("popstate", tests2, {once: true});
        history.back();
    }
    function tests2() {
        test(function () {
            assert_equals( history.length, histlength );
        }, 'history.length should not decrease after going back');
        test(function () {
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' );
        }, 'traversing history must traverse pushed states');
        iframe.contentWindow.addEventListener("hashchange", tests3, {once: true});
        history.go(-1);
    }
    function tests3() {
        test(function () {
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), '', '(this could cause other failures later on)' );
        }, 'traversing history must also traverse hash changes');

        iframe.contentWindow.addEventListener("hashchange", tests4, {once: true});
        //Safari 5.0.3 fails here - it navigates *this* document to the *iframe's* location, instead of just navigating the iframe
        history.go(2);
    }
    async function tests4() {
        test(function () {
            //Firefox 4 beta 11 has a messed up error object, which does not have the right error type or .SECURITY_ERR property
            assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','//exa mple'); });
        }, 'pushState must not be allowed to create invalid URLs');
        test(function () {
            assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','http://www.example.com/'); });
        }, 'pushState must not be allowed to create cross-origin URLs');
        test(function () {
            assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','about:blank'); });
        }, 'pushState must not be allowed to create cross-origin URLs (about:blank)');
        test(function () {
            assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','data:text/html,'); });
        }, 'pushState must not be allowed to create cross-origin URLs (data:URI)');
        test(function () {
            assert_throws_dom('SECURITY_ERR', iframe.contentWindow.DOMException, function () { iframe.contentWindow.history.pushState('','','http://www.example.com/'); });
        }, 'security errors are expected to be thrown in the context of the document that owns the history object');
        let hashchange =
          new Promise(function (resolve) {
            iframe.contentWindow.addEventListener("hashchange", resolve, {once: true});
          });

        test(function () {
            iframe.contentWindow.location.hash = 'test2';
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test2', 'location.hash did not change when told to' );
        }, 'location.hash must be allowed to change (part 1)');
        await hashchange;
        history.go(-1);
        iframe.contentWindow.addEventListener("hashchange", tests5, {once: true});
    }
    function tests5() {
        test(function () {
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test', 'location.hash did not change when going back' );
        }, 'location.hash must be allowed to change (part 2)');
        test(function () {
            iframe.contentWindow.history.pushState('','');
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test', 'location.hash changed when an unrelated state was pushed' );
        }, 'pushState must not alter location.hash when no URL is provided');
        history.go(1); //should do nothing, since the pushState should have removed the forward history
        setTimeout(tests6,50); //.go is queued to end of thread
    }
    function tests6() {
        test(function () {
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' );
        }, 'pushState must remove all history after the current state');
        test(function () {
            iframe.contentWindow.history.pushState('','','#test3');
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test3' );
        }, 'pushState must be able to set location.hash');
        //begin setup for "remove any tasks queued by the history traversal task source"
        iframe.contentWindow.location.hash = '#test4';
        iframe.contentWindow.history.go(-1); //must be queued
        try {
            //must remove the queued navigation in the same browsing context
            iframe.contentWindow.history.pushState('','');
        } catch(unsuperr) {}
        //allow the browser to mistakenly run the .go if it is going to
        //do not put two .go commands in the same thread, in case the browser mistakenly calculates the history position when
        //calling .go instead of when executing the traversal task - that could give a false PASS in the next test otherwise
        setTimeout(tests7,50);
    }
    function tests7() {
        iframe.contentWindow.history.go(-1); //must be queued, but should not be removed this time
        iframe.contentWindow.addEventListener("popstate", tests8, {once: true});
    }
    function tests8() {
        test(function () {
            assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test4' );
        }, 'pushState must remove any tasks queued by the history traversal task source');
        //end "remove any tasks queued by the history traversal task source"
        window.addEventListener('hashchange',function () { hashchng = true; },false);
        try {
            //push a state that changes the hash
            iframe.contentWindow.history.pushState('','',iframe.contentWindow.location.pathname+'#test5');
        } catch(unsuperr) {}
        setTimeout(tests9,50); //allow the hashchange event to process, if the browser has mistakenly fired it
    }
    function tests9() {
        test(function () {
            assert_false( hashchng );
        }, 'pushState must not fire hashchange events');
        test(function () {
            iframe.contentWindow.history.pushState('','','/testing_ignore_me_404');
            assert_equals( iframe.contentWindow.location.pathname, '/testing_ignore_me_404' );
        }, 'pushState must be able to set location.pathname');
        test(function () {
            var newURL = location.href.replace(/\/[^\/]*$/)+'/testing_ignore_me_404/';
            iframe.contentWindow.history.pushState('','',newURL);
            assert_equals( iframe.contentWindow.location.href, newURL );
        }, 'pushState must be able to set absolute URLs to the same host');
        test(function () {
            assert_throws_dom( 'DATA_CLONE_ERR', function () {
                history.pushState({dummy:function () {}},'');
            } );
        }, 'pushState must not be able to use a function as data');
        test(function () {
            assert_throws_dom( 'DATA_CLONE_ERR', function () {
                history.pushState({dummy:window},'');
            } );
        }, 'pushState must not be able to use a DOM node as data');
        test(function () {
            try { a.b = c; } catch(errdata) {
                history.pushState({dummy:errdata},'');
                assert_equals(ReferenceError.prototype, Object.getPrototypeOf(history.state.dummy));
            }
        }, 'pushState must be able to use an error object as data');
        test(function () {
            assert_throws_dom('DATA_CLONE_ERR', iframe.contentWindow.DOMException, function () {
                iframe.contentWindow.history.pushState(document,'');
            });
        }, 'security errors are expected to be thrown in the context of the document that owns the history object (2)');
        cloneobj = {
            nulldata: null,
            udefdata: window.undefined,
            booldata: true,
            numdata: 1,
            strdata: 'string data',
            boolobj: new Boolean(true),
            numobj: new Number(1),
            strobj: new String('string data'),
            datedata: new Date(),
            regdata: /a/g,
            arrdata: [1]
        };
        cloneobj.regdata.lastIndex = 1;
        cloneobj.looped = cloneobj;
        //test the ImageData type, if the browser supports it
        var canvas = document.createElement('canvas');
        if( canvas.getContext && ( canvas = canvas.getContext('2d') ) && canvas.createImageData ) {
            canvassup = true;
            cloneobj.imgdata = canvas.createImageData(1,1);
        }
        test(function () {
            try {
                iframe.contentWindow.history.pushState(cloneobj,'new title');
            } catch(e) {
                cloneobj.looped = null;
                //try again because this object is needed for future tests
                iframe.contentWindow.history.pushState(cloneobj,'new title');
                //rethrow so the browser gets a FAIL for not coping with the circular reference; "internal structured cloning algorithm" step 1
                throw(e);
            }
        }, 'pushState must be able to make structured clones of complex objects');
        test(function () {
            assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' );
        }, 'history.state should also reference a clone of the original object');
        test(function () {
            assert_not_equals( cloneobj, iframe.contentWindow.history.state );
        }, 'history.state should be a clone of the original object, not a reference to it');
        /*
        behaviour is not defined per spec, and no known implementations do this
        test(function () {
            assert_equals( iframe.contentDocument.title, 'new title', 'not required for specification conformance' );
        }, 'pushState MIGHT set the document title');
        */
        history.go(-1);
        iframe.contentWindow.addEventListener("popstate", tests10, {once: true});
    }
    function tests10() {
        var eventtime = setTimeout(function () { tests11(false); },500); //should be cleared by the event handler long before it has a chance to fire
        iframe.contentWindow.addEventListener('popstate',function (e) { clearTimeout(eventtime); tests11(true,e); },false);
        history.forward();
    }
    function tests11(hasFired,ev) {
        test(function () {
            assert_true( hasFired );
        }, 'popstate event should fire when navigation occurs');
        test(function () {
            assert_true( !!ev && typeof(ev.state) != 'undefined', 'state information was not passed' );
            assert_true( !!ev.state, 'state information does not contain the expected value - browser is probably stuck in the wrong history position' );
            assert_equals( ev.state.nulldata, null, 'state null data was not correct' );
            assert_equals( ev.state.udefdata, window.undefined, 'state undefined data was not correct' );
            assert_true( ev.state.booldata, 'state boolean data was not correct' );
            assert_equals( ev.state.numdata, 1, 'state numeric data was not correct' );
            assert_equals( ev.state.strdata, 'string data', 'state string data was not correct' );
            assert_true( !!ev.state.datedata.getTime, 'state date data was not correct' );
            assert_own_property( ev.state, 'regdata', 'state regex data was not correct' );
            assert_equals( ev.state.regdata.source, 'a', 'state regex pattern data was not correct' );
            assert_true( ev.state.regdata.global, 'state regex flag data was not correct' );
            assert_equals( ev.state.regdata.lastIndex, 0, 'state regex lastIndex data was not correct' );
            assert_equals( ev.state.arrdata.length, 1, 'state array data was not correct' );
            assert_true( ev.state.boolobj.valueOf(), 'state boolean data was not correct' );
            assert_equals( ev.state.numobj.valueOf(), 1, 'state numeric data was not correct' );
            assert_equals( ev.state.strobj.valueOf(), 'string data', 'state string data was not correct' );
            if( canvassup ) {
                assert_equals( ev.state.imgdata.width, 1, 'state ImageData was not correct' );
            }
        }, 'popstate event should pass the state data');
        test(function () {
            assert_equals( ev.state.looped, ev.state );
        }, 'state data should cope with circular object references');
        test(function () {
            assert_not_equals( cloneobj, ev.state );
        }, 'state data should be a clone of the original object, not a reference to it');
        test(function () {
            assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' );
        }, 'history.state should also reference a clone of the original object (2)');
        test(function () {
            assert_not_equals( cloneobj, iframe.contentWindow.history.state );
        }, 'history.state should be a clone of the original object, not a reference to it (2)');
        test(function () {
            assert_equals( iframe.contentWindow.history.state, ev.state );
        }, 'history.state should be identical to the object passed to the event handler unless history.state is updated');
        try {
            iframe.contentWindow.persistval = true;
            iframe.contentWindow.history.pushState('','', location.href.replace(/\/[^\/]*$/,'/blank3.html') );
        } catch(unsuperr) {}
        //it's already cached, so this should be very fast if the browser mistakenly loads it
        //it should not need to load at all, since it's just a pushed state
        setTimeout(tests12,1000);
    }
    function tests12() {
        test(function () {
            assert_true( iframe.contentWindow.persistval && !iframe.contentWindow.forreal );
        }, 'pushState should not actually load the new URL');
        atstep = 3;
        iframe.contentWindow.location.reload(); //load the real URL
        lasttimer = setTimeout(function () { tests13(false); },3000); //should be cleared by the onload handler long before it has a chance to fire
    }
    function tests13(passed) {
        test(function () {
            assert_true( passed, 'expected a load event to fire when reloading the URL from cache, gave up waiting after 3 seconds' );
        }, 'reloading a pushed state should actually load the new URL');
        //try to make browsers behave when reloading so that the correct URL is recovered - does not always work
        iframe.contentWindow.location.href = location.href.replace(/\/[^\/]*$/,'/blank.html');
        done();
    }

    if( atstep == 1 ) {
        //blank2 has loaded
        atstep = 2;
        //use a timeout, because some browsers intentionally do not add history entries for URL changes in an onload thread
        setTimeout(tests1,100);
    } else if( atstep == 3 ) {
        //blank3 should now have loaded after the .reload() command
        atstep = 4;
        clearTimeout(lasttimer);
        tests13(true);
    }
}



        </script>
    </head>
    <body>

        <noscript><p>Enable JavaScript and reload</p></noscript>
        <p>WARNING: This test should always be loaded in a new tab/window, to avoid browsers attempting to recover the state of frames, and history length. Do not reload the test.</p>
        <div id="log">Running test...</div>
        <p><iframe onload="reportload();" src="blank.html"></iframe></p>
        <p><iframe src="blank.html"></iframe></p>
        <p><iframe src="blank2.html"></iframe></p>
        <p><iframe src="blank3.html"></iframe></p>

    </body>
</html>
