001.html (19345B)
1 <!doctype html> 2 <html> 3 <head> 4 <title>history.pushState 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 function tests1() { 40 //Firefox may fail when reloading, because it recovers iframe state, and therefore does not see the need to alter history length 41 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'); 42 histlength = history.length; 43 iframe.contentWindow.location.hash = 'test'; //should be synchronous **SEE COMMENT AT TOP OF FILE 44 test(function () { 45 assert_equals( history.length, histlength + 1, 'make sure that you loaded the test in a new tab/window' ); 46 }, 'history.length should update when setting location.hash'); 47 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 48 test(function () { assert_true( !!iframe.contentWindow.history.pushState, 'critical test; ignore any failures after this' ); }, 'history.pushState must exist within iframes'); 49 test(function () { 50 assert_equals( iframe.contentWindow.history.state, null ); 51 }, 'initial history.state should be null'); 52 test(function () { 53 histlength = history.length; 54 iframe.contentWindow.history.pushState('',''); 55 assert_equals( history.length, histlength + 1 ); 56 }, 'history.length should update when pushing a state'); 57 test(function () { 58 assert_equals( iframe.contentWindow.history.state, '' ); 59 }, 'history.state should update after a state is pushed'); 60 histlength = history.length; 61 iframe.contentWindow.addEventListener("popstate", tests2, {once: true}); 62 history.back(); 63 } 64 function tests2() { 65 test(function () { 66 assert_equals( history.length, histlength ); 67 }, 'history.length should not decrease after going back'); 68 test(function () { 69 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' ); 70 }, 'traversing history must traverse pushed states'); 71 iframe.contentWindow.addEventListener("hashchange", tests3, {once: true}); 72 history.go(-1); 73 } 74 function tests3() { 75 test(function () { 76 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), '', '(this could cause other failures later on)' ); 77 }, 'traversing history must also traverse hash changes'); 78 79 iframe.contentWindow.addEventListener("hashchange", tests4, {once: true}); 80 //Safari 5.0.3 fails here - it navigates *this* document to the *iframe's* location, instead of just navigating the iframe 81 history.go(2); 82 } 83 async function tests4() { 84 test(function () { 85 //Firefox 4 beta 11 has a messed up error object, which does not have the right error type or .SECURITY_ERR property 86 assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','//exa mple'); }); 87 }, 'pushState must not be allowed to create invalid URLs'); 88 test(function () { 89 assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','http://www.example.com/'); }); 90 }, 'pushState must not be allowed to create cross-origin URLs'); 91 test(function () { 92 assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','about:blank'); }); 93 }, 'pushState must not be allowed to create cross-origin URLs (about:blank)'); 94 test(function () { 95 assert_throws_dom('SECURITY_ERR',function () { history.pushState('','','data:text/html,'); }); 96 }, 'pushState must not be allowed to create cross-origin URLs (data:URI)'); 97 test(function () { 98 assert_throws_dom('SECURITY_ERR', iframe.contentWindow.DOMException, function () { iframe.contentWindow.history.pushState('','','http://www.example.com/'); }); 99 }, 'security errors are expected to be thrown in the context of the document that owns the history object'); 100 let hashchange = 101 new Promise(function (resolve) { 102 iframe.contentWindow.addEventListener("hashchange", resolve, {once: true}); 103 }); 104 105 test(function () { 106 iframe.contentWindow.location.hash = 'test2'; 107 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test2', 'location.hash did not change when told to' ); 108 }, 'location.hash must be allowed to change (part 1)'); 109 await hashchange; 110 history.go(-1); 111 iframe.contentWindow.addEventListener("hashchange", tests5, {once: true}); 112 } 113 function tests5() { 114 test(function () { 115 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test', 'location.hash did not change when going back' ); 116 }, 'location.hash must be allowed to change (part 2)'); 117 test(function () { 118 iframe.contentWindow.history.pushState('',''); 119 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test', 'location.hash changed when an unrelated state was pushed' ); 120 }, 'pushState must not alter location.hash when no URL is provided'); 121 history.go(1); //should do nothing, since the pushState should have removed the forward history 122 setTimeout(tests6,50); //.go is queued to end of thread 123 } 124 function tests6() { 125 test(function () { 126 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test' ); 127 }, 'pushState must remove all history after the current state'); 128 test(function () { 129 iframe.contentWindow.history.pushState('','','#test3'); 130 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test3' ); 131 }, 'pushState must be able to set location.hash'); 132 //begin setup for "remove any tasks queued by the history traversal task source" 133 iframe.contentWindow.location.hash = '#test4'; 134 iframe.contentWindow.history.go(-1); //must be queued 135 try { 136 //must remove the queued navigation in the same browsing context 137 iframe.contentWindow.history.pushState('',''); 138 } catch(unsuperr) {} 139 //allow the browser to mistakenly run the .go if it is going to 140 //do not put two .go commands in the same thread, in case the browser mistakenly calculates the history position when 141 //calling .go instead of when executing the traversal task - that could give a false PASS in the next test otherwise 142 setTimeout(tests7,50); 143 } 144 function tests7() { 145 iframe.contentWindow.history.go(-1); //must be queued, but should not be removed this time 146 iframe.contentWindow.addEventListener("popstate", tests8, {once: true}); 147 } 148 function tests8() { 149 test(function () { 150 assert_equals( iframe.contentWindow.location.hash.replace(/^#/,''), 'test4' ); 151 }, 'pushState must remove any tasks queued by the history traversal task source'); 152 //end "remove any tasks queued by the history traversal task source" 153 window.addEventListener('hashchange',function () { hashchng = true; },false); 154 try { 155 //push a state that changes the hash 156 iframe.contentWindow.history.pushState('','',iframe.contentWindow.location.pathname+'#test5'); 157 } catch(unsuperr) {} 158 setTimeout(tests9,50); //allow the hashchange event to process, if the browser has mistakenly fired it 159 } 160 function tests9() { 161 test(function () { 162 assert_false( hashchng ); 163 }, 'pushState must not fire hashchange events'); 164 test(function () { 165 iframe.contentWindow.history.pushState('','','/testing_ignore_me_404'); 166 assert_equals( iframe.contentWindow.location.pathname, '/testing_ignore_me_404' ); 167 }, 'pushState must be able to set location.pathname'); 168 test(function () { 169 var newURL = location.href.replace(/\/[^\/]*$/)+'/testing_ignore_me_404/'; 170 iframe.contentWindow.history.pushState('','',newURL); 171 assert_equals( iframe.contentWindow.location.href, newURL ); 172 }, 'pushState must be able to set absolute URLs to the same host'); 173 test(function () { 174 assert_throws_dom( 'DATA_CLONE_ERR', function () { 175 history.pushState({dummy:function () {}},''); 176 } ); 177 }, 'pushState must not be able to use a function as data'); 178 test(function () { 179 assert_throws_dom( 'DATA_CLONE_ERR', function () { 180 history.pushState({dummy:window},''); 181 } ); 182 }, 'pushState must not be able to use a DOM node as data'); 183 test(function () { 184 try { a.b = c; } catch(errdata) { 185 history.pushState({dummy:errdata},''); 186 assert_equals(ReferenceError.prototype, Object.getPrototypeOf(history.state.dummy)); 187 } 188 }, 'pushState must be able to use an error object as data'); 189 test(function () { 190 assert_throws_dom('DATA_CLONE_ERR', iframe.contentWindow.DOMException, function () { 191 iframe.contentWindow.history.pushState(document,''); 192 }); 193 }, 'security errors are expected to be thrown in the context of the document that owns the history object (2)'); 194 cloneobj = { 195 nulldata: null, 196 udefdata: window.undefined, 197 booldata: true, 198 numdata: 1, 199 strdata: 'string data', 200 boolobj: new Boolean(true), 201 numobj: new Number(1), 202 strobj: new String('string data'), 203 datedata: new Date(), 204 regdata: /a/g, 205 arrdata: [1] 206 }; 207 cloneobj.regdata.lastIndex = 1; 208 cloneobj.looped = cloneobj; 209 //test the ImageData type, if the browser supports it 210 var canvas = document.createElement('canvas'); 211 if( canvas.getContext && ( canvas = canvas.getContext('2d') ) && canvas.createImageData ) { 212 canvassup = true; 213 cloneobj.imgdata = canvas.createImageData(1,1); 214 } 215 test(function () { 216 try { 217 iframe.contentWindow.history.pushState(cloneobj,'new title'); 218 } catch(e) { 219 cloneobj.looped = null; 220 //try again because this object is needed for future tests 221 iframe.contentWindow.history.pushState(cloneobj,'new title'); 222 //rethrow so the browser gets a FAIL for not coping with the circular reference; "internal structured cloning algorithm" step 1 223 throw(e); 224 } 225 }, 'pushState must be able to make structured clones of complex objects'); 226 test(function () { 227 assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' ); 228 }, 'history.state should also reference a clone of the original object'); 229 test(function () { 230 assert_not_equals( cloneobj, iframe.contentWindow.history.state ); 231 }, 'history.state should be a clone of the original object, not a reference to it'); 232 /* 233 behaviour is not defined per spec, and no known implementations do this 234 test(function () { 235 assert_equals( iframe.contentDocument.title, 'new title', 'not required for specification conformance' ); 236 }, 'pushState MIGHT set the document title'); 237 */ 238 history.go(-1); 239 iframe.contentWindow.addEventListener("popstate", tests10, {once: true}); 240 } 241 function tests10() { 242 var eventtime = setTimeout(function () { tests11(false); },500); //should be cleared by the event handler long before it has a chance to fire 243 iframe.contentWindow.addEventListener('popstate',function (e) { clearTimeout(eventtime); tests11(true,e); },false); 244 history.forward(); 245 } 246 function tests11(hasFired,ev) { 247 test(function () { 248 assert_true( hasFired ); 249 }, 'popstate event should fire when navigation occurs'); 250 test(function () { 251 assert_true( !!ev && typeof(ev.state) != 'undefined', 'state information was not passed' ); 252 assert_true( !!ev.state, 'state information does not contain the expected value - browser is probably stuck in the wrong history position' ); 253 assert_equals( ev.state.nulldata, null, 'state null data was not correct' ); 254 assert_equals( ev.state.udefdata, window.undefined, 'state undefined data was not correct' ); 255 assert_true( ev.state.booldata, 'state boolean data was not correct' ); 256 assert_equals( ev.state.numdata, 1, 'state numeric data was not correct' ); 257 assert_equals( ev.state.strdata, 'string data', 'state string data was not correct' ); 258 assert_true( !!ev.state.datedata.getTime, 'state date data was not correct' ); 259 assert_own_property( ev.state, 'regdata', 'state regex data was not correct' ); 260 assert_equals( ev.state.regdata.source, 'a', 'state regex pattern data was not correct' ); 261 assert_true( ev.state.regdata.global, 'state regex flag data was not correct' ); 262 assert_equals( ev.state.regdata.lastIndex, 0, 'state regex lastIndex data was not correct' ); 263 assert_equals( ev.state.arrdata.length, 1, 'state array data was not correct' ); 264 assert_true( ev.state.boolobj.valueOf(), 'state boolean data was not correct' ); 265 assert_equals( ev.state.numobj.valueOf(), 1, 'state numeric data was not correct' ); 266 assert_equals( ev.state.strobj.valueOf(), 'string data', 'state string data was not correct' ); 267 if( canvassup ) { 268 assert_equals( ev.state.imgdata.width, 1, 'state ImageData was not correct' ); 269 } 270 }, 'popstate event should pass the state data'); 271 test(function () { 272 assert_equals( ev.state.looped, ev.state ); 273 }, 'state data should cope with circular object references'); 274 test(function () { 275 assert_not_equals( cloneobj, ev.state ); 276 }, 'state data should be a clone of the original object, not a reference to it'); 277 test(function () { 278 assert_equals( iframe.contentWindow.history.state && iframe.contentWindow.history.state.strdata, 'string data' ); 279 }, 'history.state should also reference a clone of the original object (2)'); 280 test(function () { 281 assert_not_equals( cloneobj, iframe.contentWindow.history.state ); 282 }, 'history.state should be a clone of the original object, not a reference to it (2)'); 283 test(function () { 284 assert_equals( iframe.contentWindow.history.state, ev.state ); 285 }, 'history.state should be identical to the object passed to the event handler unless history.state is updated'); 286 try { 287 iframe.contentWindow.persistval = true; 288 iframe.contentWindow.history.pushState('','', location.href.replace(/\/[^\/]*$/,'/blank3.html') ); 289 } catch(unsuperr) {} 290 //it's already cached, so this should be very fast if the browser mistakenly loads it 291 //it should not need to load at all, since it's just a pushed state 292 setTimeout(tests12,1000); 293 } 294 function tests12() { 295 test(function () { 296 assert_true( iframe.contentWindow.persistval && !iframe.contentWindow.forreal ); 297 }, 'pushState should not actually load the new URL'); 298 atstep = 3; 299 iframe.contentWindow.location.reload(); //load the real URL 300 lasttimer = setTimeout(function () { tests13(false); },3000); //should be cleared by the onload handler long before it has a chance to fire 301 } 302 function tests13(passed) { 303 test(function () { 304 assert_true( passed, 'expected a load event to fire when reloading the URL from cache, gave up waiting after 3 seconds' ); 305 }, 'reloading a pushed state should actually load the new URL'); 306 //try to make browsers behave when reloading so that the correct URL is recovered - does not always work 307 iframe.contentWindow.location.href = location.href.replace(/\/[^\/]*$/,'/blank.html'); 308 done(); 309 } 310 311 if( atstep == 1 ) { 312 //blank2 has loaded 313 atstep = 2; 314 //use a timeout, because some browsers intentionally do not add history entries for URL changes in an onload thread 315 setTimeout(tests1,100); 316 } else if( atstep == 3 ) { 317 //blank3 should now have loaded after the .reload() command 318 atstep = 4; 319 clearTimeout(lasttimer); 320 tests13(true); 321 } 322 } 323 324 325 326 </script> 327 </head> 328 <body> 329 330 <noscript><p>Enable JavaScript and reload</p></noscript> 331 <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> 332 <div id="log">Running test...</div> 333 <p><iframe onload="reportload();" src="blank.html"></iframe></p> 334 <p><iframe src="blank.html"></iframe></p> 335 <p><iframe src="blank2.html"></iframe></p> 336 <p><iframe src="blank3.html"></iframe></p> 337 338 </body> 339 </html>