002.html (18072B)
1 <!doctype html> 2 <html> 3 <head> 4 <title>history.replaceState tests</title> 5 <script type="text/javascript" src="/resources/testharness.js"></script> 6 <script type="text/javascript" src="/resources/testharnessreport.js"></script> 7 <script type="text/javascript"> 8 //does not test for firing of popstate onload, because this was dropped from the specification on 25 March 2011 9 //covers history.state after load, in accordance with the specification draft from 25 March 2011 10 //history.state before load is tested in 006 and 007 11 //does not test for structured cloning of FileList, File or Blob interfaces, as these require manual file selection 12 13 //**This test assumes that assignments to location.hash will be synchronous - this is how all browsers implement it. 14 //The spec (as of 25 March 2011) disagrees. 15 16 var histlength, atstep = 0, lasttimer; 17 setup({explicit_done:true}); //tests should take under 6 seconds + execution time 18 19 window.onload = function () { 20 if( location.protocol == 'file:' ) { 21 document.getElementsByTagName('p')[0].innerHTML = 'ERROR: This test cannot be run from file: (URL resolving will not work). It must be loaded over HTTP.'; 22 return; 23 } else if( location.protocol == 'https:' ) { 24 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.'; 25 } 26 //use a timeout, because some browsers intentionally do not add history entries for URL changes in the onload thread 27 setTimeout(testinit,100); 28 }; 29 function testinit() { 30 atstep = 1; 31 histlength = history.length; 32 iframe = document.getElementsByTagName('iframe')[0].src = 'blank2.html'; 33 //reportload will now be called by the onload handler for the iframe 34 } 35 function reportload() { 36 var iframe = document.getElementsByTagName('iframe')[0], hashchng = false; 37 var canvassup = false, cloneobj; 38 39 async function tests1() { 40 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'); 41 histlength = history.length; 42 let hashchange = new Promise(function(resolve) { 43 iframe.contentWindow.addEventListener("hashchange", resolve, {once: true}); 44 }); 45 iframe.contentWindow.location.hash = 'test'; //should be synchronous **SEE COMMENT AT TOP OF FILE 46 test(function () { 47 assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' ); 48 }, 'history.length should update when setting location.hash'); 49 test(function () { assert_true( !!history.replaceState, 'critical test; ignore any failures after this' ); }, 'history.replaceState must exist'); //assert_own_property does not allow prototype inheritance 50 test(function () { assert_true( !!iframe.contentWindow.history.replaceState, 'critical test; ignore any failures after this' ); }, 'history.replaceState must exist within iframes'); 51 test(function () { 52 assert_equals( iframe.contentWindow.history.state, null ); 53 }, 'initial history.state should be null'); 54 55 await hashchange; 56 hashchange = new Promise(function(resolve) { 57 iframe.contentWindow.addEventListener("hashchange", resolve, {once: true}); 58 }); 59 iframe.contentWindow.location.hash = 'test2'; 60 await hashchange; 61 62 iframe.contentWindow.addEventListener("hashchange", tests2, {once: true}); 63 history.back(); 64 } 65 function tests2() { 66 test(function () { 67 histlength = history.length; 68 iframe.contentWindow.history.replaceState('',''); 69 assert_equals( history.length, histlength ); 70 }, 'history.length should not update when replacing a state with no URL'); 71 test(function () { 72 assert_equals( iframe.contentWindow.history.state, '' ); 73 }, 'history.state should update after a state is pushed'); 74 test(function () { 75 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' ); 76 }, 'hash should not change when replaceState is called without a URL'); 77 test(function () { 78 histlength = history.length; 79 iframe.contentWindow.history.replaceState('','','#test3'); 80 assert_equals( history.length, histlength ); 81 }, 'history.length should not update when replacing a state with a URL'); 82 test(function () { 83 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test3' ); 84 }, 'hash should change when replaceState is called with a URL'); 85 iframe.contentWindow.addEventListener("hashchange", tests3, {once: true}); 86 history.go(-1); 87 } 88 function tests3() { 89 test(function () { 90 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), '' ); 91 }, 'replaceState must replace the existing state and not add an extra one'); 92 iframe.contentWindow.addEventListener("hashchange", tests4, {once: true}); 93 history.go(2); 94 } 95 function tests4() { 96 test(function () { 97 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test2' ); 98 }, 'replaceState must replace the existing state without altering the forward history'); 99 test(function () { 100 assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','//exa mple'); }); 101 }, 'replaceState must not be allowed to create invalid URLs'); 102 test(function () { 103 assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','http://www.example.com/'); }); 104 }, 'replaceState must not be allowed to create cross-origin URLs'); 105 test(function () { 106 assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','about:blank'); }); 107 }, 'replaceState must not be allowed to create cross-origin URLs (about:blank)'); 108 test(function () { 109 assert_throws_dom('SECURITY_ERR',function () { history.replaceState('','','data:text/html,'); }); 110 }, 'replaceState must not be allowed to create cross-origin URLs (data:URI)'); 111 test(function () { 112 assert_throws_dom('SECURITY_ERR',iframe.contentWindow.DOMException,function () { iframe.contentWindow.history.replaceState('','','http://www.example.com/'); }); 113 }, 'security errors are expected to be thrown in the context of the document that owns the history object'); 114 test(function () { 115 //avoids browsers running .go synchronously when only a hash change is involved 116 iframe.contentWindow.history.replaceState('','','/testing_ignore_me_404#test4'); 117 assert_equals( iframe.contentWindow.location.pathname, '/testing_ignore_me_404' ); 118 }, 'replaceState must be able to set location.pathname'); 119 test(function () { 120 var newURL = location.href.replace(/\/[^\/]*$/)+'/testing_ignore_me_404/'; 121 iframe.contentWindow.history.replaceState('','',newURL); 122 assert_equals( iframe.contentWindow.location.href, newURL ); 123 }, 'replaceState must be able to set absolute URLs to the same host'); 124 125 //allow the browser to run the .go 126 iframe.contentWindow.addEventListener("popstate", tests5, {once: true}); 127 //begin setup for "[must not] remove any tasks queued by the history traversal task source" 128 iframe.contentWindow.history.go(-1); //must be queued so the next command takes place *beforehand* 129 try { 130 //must not remove the queued navigation in the same browsing context 131 iframe.contentWindow.history.replaceState('','',iframe.contentWindow.location.pathname+'#test5'); 132 } catch(unsuperr2) {} 133 } 134 function tests5() { 135 test(function () { 136 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test3' ); 137 }, 'replaceState must not remove any tasks queued by the history traversal task source'); 138 iframe.contentWindow.addEventListener("popstate", tests6, {once: true}); 139 //Safari 5.0.3 fails here - it navigates *this* document to the *iframe's* location, instead of just navigating the iframe 140 history.go(1); 141 } 142 function tests6() { 143 test(function () { 144 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test5' ); 145 }, '.go must queue a task with the history traversal task source (run asynchronously)'); 146 //end "[must not] remove any tasks queued by the history traversal task source" 147 window.addEventListener('hashchange',function () { hashchng = true; },false); 148 try { 149 //push a state that changes the hash 150 iframe.contentWindow.history.replaceState('','',iframe.contentWindow.location.pathname+'#test6'); 151 } catch(unsuperr) {} 152 setTimeout(tests7,50); //allow the hashchange event to process, if the browser has mistakenly fired it 153 } 154 function tests7() { 155 test(function () { 156 assert_false( hashchng ); 157 }, 'replaceState must not fire hashchange events'); 158 test(function () { 159 assert_throws_dom( 'DATA_CLONE_ERR', function () { 160 history.replaceState({dummy:function () {}},''); 161 } ); 162 }, 'replaceState must not be able to use a function as data'); 163 test(function () { 164 assert_throws_dom( 'DATA_CLONE_ERR', function () { 165 history.replaceState({dummy:window},''); 166 } ); 167 }, 'replaceState must not be able to use a DOM node as data'); 168 test(function () { 169 try { a.b = c; } catch(errdata) { 170 history.replaceState({dummy:errdata},''); 171 assert_equals(ReferenceError.prototype, Object.getPrototypeOf(history.state.dummy)); 172 } 173 }, 'replaceState must be able to use an error object as data'); 174 test(function () { 175 assert_throws_dom('DATA_CLONE_ERR', iframe.contentWindow.DOMException, function () { 176 iframe.contentWindow.history.replaceState(document,''); 177 }); 178 }, 'security errors are expected to be thrown in the context of the document that owns the history object (2)'); 179 cloneobj = { 180 nulldata: null, 181 udefdata: window.undefined, 182 booldata: true, 183 numdata: 1, 184 strdata: 'string data', 185 boolobj: new Boolean(true), 186 numobj: new Number(1), 187 strobj: new String('string data'), 188 datedata: new Date(), 189 regdata: /a/g, 190 arrdata: [1] 191 }; 192 cloneobj.regdata.lastIndex = 1; 193 cloneobj.looped = cloneobj; 194 //test the ImageData type, if the browser supports it 195 var canvas = document.createElement('canvas'); 196 if( canvas.getContext && ( canvas = canvas.getContext('2d') ) && canvas.createImageData ) { 197 canvassup = true; 198 cloneobj.imgdata = canvas.createImageData(1,1); 199 } 200 test(function () { 201 try { 202 iframe.contentWindow.history.replaceState(cloneobj,'new title'); 203 } catch(e) { 204 cloneobj.looped = null; 205 //try again because this object is needed for future tests 206 iframe.contentWindow.history.replaceState(cloneobj,'new title'); 207 //rethrow so the browser gets a FAIL for not coping with the circular reference; "internal structured cloning algorithm" step 1 208 throw(e); 209 } 210 }, 'replaceState must be able to make structured clones of complex objects'); 211 test(function () { 212 assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' ); 213 }, 'history.state should also reference a clone of the original object'); 214 test(function () { 215 assert_not_equals( cloneobj, iframe.contentWindow.history.state ); 216 }, 'history.state should be a clone of the original object, not a reference to it'); 217 iframe.contentWindow.addEventListener("popstate", tests8, {once: true}); 218 history.go(-1); 219 } 220 function tests8() { 221 var eventtime = setTimeout(function () { tests9(false); },500); //should be cleared by the event handler long before it has a chance to fire 222 iframe.contentWindow.addEventListener('popstate',function (e) { clearTimeout(eventtime); tests9(true,e); },false); 223 history.forward(); 224 } 225 function tests9(hasFired,ev) { 226 test(function () { 227 assert_true( hasFired ); 228 }, 'popstate event should fire when navigation occurs'); 229 test(function () { 230 assert_true( !!ev && typeof(ev.state) != 'undefined', 'state information was not passed' ); 231 assert_true( !!ev.state, 'state information does not contain the expected value - browser is probably stuck in the wrong history position' ); 232 assert_equals( ev.state.nulldata, null, 'state null data was not correct' ); 233 assert_equals( ev.state.udefdata, window.undefined, 'state undefined data was not correct' ); 234 assert_true( ev.state.booldata, 'state boolean data was not correct' ); 235 assert_equals( ev.state.numdata, 1, 'state numeric data was not correct' ); 236 assert_equals( ev.state.strdata, 'string data', 'state string data was not correct' ); 237 assert_true( !!ev.state.datedata.getTime, 'state date data was not correct' ); 238 assert_own_property( ev.state, 'regdata', 'state regex data was not correct' ); 239 assert_equals( ev.state.regdata.source, 'a', 'state regex pattern data was not correct' ); 240 assert_true( ev.state.regdata.global, 'state regex flag data was not correct' ); 241 assert_equals( ev.state.regdata.lastIndex, 0, 'state regex lastIndex data was not correct' ); 242 assert_equals( ev.state.arrdata.length, 1, 'state array data was not correct' ); 243 assert_true( ev.state.boolobj.valueOf(), 'state boolean data was not correct' ); 244 assert_equals( ev.state.numobj.valueOf(), 1, 'state numeric data was not correct' ); 245 assert_equals( ev.state.strobj.valueOf(), 'string data', 'state string data was not correct' ); 246 if( canvassup ) { 247 assert_equals( ev.state.imgdata.width, 1, 'state ImageData was not correct' ); 248 } 249 }, 'popstate event should pass the state data'); 250 test(function () { 251 assert_equals( ev.state.looped, ev.state ); 252 }, 'state data should cope with circular object references'); 253 test(function () { 254 assert_not_equals( cloneobj, ev.state ); 255 }, 'state data should be a clone of the original object, not a reference to it'); 256 test(function () { 257 assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' ); 258 }, 'history.state should also reference a clone of the original object (2)'); 259 test(function () { 260 assert_not_equals( cloneobj, iframe.contentWindow.history.state ); 261 }, 'history.state should be a clone of the original object, not a reference to it (2)'); 262 test(function () { 263 assert_equals( iframe.contentWindow.history.state, ev.state ); 264 }, 'history.state should be identical to the object passed to the event handler unless history.state is updated'); 265 try { 266 iframe.contentWindow.persistval = true; 267 iframe.contentWindow.history.replaceState('','', location.href.replace(/\/[^\/]*$/,'/blank3.html') ); 268 } catch(unsuperr) {} 269 //it's already cached, so this should be very fast if the browser mistakenly loads it 270 //it should not need to load at all, since it's just a pushed state 271 setTimeout(tests10,1000); 272 } 273 function tests10() { 274 test(function () { 275 assert_true( iframe.contentWindow.persistval && !iframe.contentWindow.forreal ); 276 }, 'replaceState should not actually load the new URL'); 277 atstep = 3; 278 iframe.contentWindow.location.reload(); //load the real URL 279 lasttimer = setTimeout(function () { tests11(false); },3000); //should be cleared by the onload handler long before it has a chance to fire 280 } 281 function tests11(passed) { 282 test(function () { 283 assert_true( passed, 'expected a load event to fire when reloading the URL from cache, gave up waiting after 3 seconds' ); 284 }, 'reloading a replaced state should actually load the new URL'); 285 //try to make browsers behave when reloading so that the correct URL is recovered - does not always work 286 iframe.contentWindow.location.href = location.href.replace(/\/[^\/]*$/,'/blank.html'); 287 done(); 288 } 289 290 if( atstep == 1 ) { 291 //blank2 has loaded 292 atstep = 2; 293 //use a timeout, because some browsers intentionally do not add history entries for URL changes in an onload thread 294 setTimeout(tests1,100); 295 } else if( atstep == 3 ) { 296 //blank3 should now have loaded after the .reload() command 297 atstep = 4; 298 clearTimeout(lasttimer); 299 tests11(true); 300 } 301 } 302 303 304 305 </script> 306 </head> 307 <body> 308 309 <noscript><p>Enable JavaScript and reload</p></noscript> 310 <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> 311 <div id="log">Running test...</div> 312 <p><iframe onload="reportload();" src="blank.html"></iframe></p> 313 <p><iframe src="blank.html"></iframe></p> 314 <p><iframe src="blank2.html"></iframe></p> 315 <p><iframe src="blank3.html"></iframe></p> 316 317 </body> 318 </html>