1 /*
   2  * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package test.javafx.scene.web;
  27 
  28 import static javafx.concurrent.Worker.State.SUCCEEDED;
  29 
  30 import java.util.concurrent.atomic.AtomicBoolean;
  31 import java.util.concurrent.CountDownLatch;
  32 import java.util.List;
  33 import java.io.File;
  34 import java.net.MalformedURLException;
  35 import javafx.beans.value.ObservableValue;
  36 import javafx.scene.web.WebEngine;
  37 import javafx.scene.web.WebHistory;
  38 import javafx.beans.value.ChangeListener;
  39 import javafx.beans.value.ObservableValue;
  40 import javafx.collections.ListChangeListener;
  41 import java.util.Date;
  42 
  43 import java.util.concurrent.Callable;
  44 import javafx.scene.web.WebHistory;
  45 import org.junit.Test;
  46 import static org.junit.Assert.assertEquals;
  47 import static org.junit.Assert.assertFalse;
  48 import static org.junit.Assert.assertNull;
  49 import static org.junit.Assert.assertTrue;
  50 import static org.junit.Assert.fail;
  51 
  52 public class HistoryTest extends TestBase {
  53     WebHistory history = getEngine().getHistory();
  54 
  55     AtomicBoolean entriesChanged = new AtomicBoolean(false);
  56     AtomicBoolean titleChanged = new AtomicBoolean(false);
  57     AtomicBoolean dateChanged = new AtomicBoolean(false);
  58     AtomicBoolean indexChanged = new AtomicBoolean(false);
  59 
  60     @Test public void test() {
  61 
  62         //
  63         // before history is populated
  64         //
  65         submit(() -> {
  66             try {
  67                 history.go(-1);
  68                 fail("go: IndexOutOfBoundsException is not thrown");
  69             } catch (IndexOutOfBoundsException ex) {}
  70             try {
  71                 history.go(1);
  72                 fail("go: IndexOutOfBoundsException is not thrown");
  73             } catch (IndexOutOfBoundsException ex) {}
  74 
  75             history.setMaxSize(99);
  76             assertEquals("max size is wrong", history.getMaxSize(), 99);
  77         });
  78 
  79         // [1*]
  80         checkLoad(new File("src/test/resources/test/html/h1.html"), 1, 0, "1");
  81 
  82         // [1, 2*]
  83         checkLoad(new File("src/test/resources/test/html/h2.html"), 2, 1, "2");
  84 
  85         //
  86         // check the list update
  87         //
  88         history.getEntries().addListener(new ListChangeListener<WebHistory.Entry>() {
  89             public void onChanged(ListChangeListener.Change<? extends WebHistory.Entry> c) {
  90                 c.next();
  91                 assertTrue("entries: change is wrong", c.wasAdded());
  92                 assertTrue("entries: size is wrong", c.getAddedSubList().size() == 1);
  93                 history.getEntries().removeListener(this);
  94                 entriesChanged.set(true);
  95             }
  96         });
  97 
  98         // [1, 2, 3*]
  99         checkLoad(new File("src/test/resources/test/html/h3.html"), 3, 2, "3");
 100 
 101         ensureValueChanged(entriesChanged, "entries not changed after load");
 102 
 103         //
 104         // check the title update
 105         //
 106         history.getEntries().get(history.getCurrentIndex()).titleProperty().addListener(new ChangeListener<String>() {
 107             public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
 108                 assertEquals("entries: old title is wrong", "3", oldValue);
 109                 assertEquals("entries: new title is wrong", "hello", newValue);
 110                 observable.removeListener(this);
 111                 titleChanged.set(true);
 112             }
 113         });
 114         executeScript("document.title='hello'");
 115 
 116         ensureValueChanged(titleChanged, "title not changed from JS");
 117 
 118         //
 119         // check the date & index updates
 120         //
 121         history.getEntries().get(history.getCurrentIndex() - 1).lastVisitedDateProperty().addListener(newDateListener());
 122 
 123         try { Thread.sleep(150); } catch (Exception e) {} // ensure the next date doesn't fit into the same millisecond
 124 
 125         history.currentIndexProperty().addListener(new ChangeListener<Number>() {
 126             public void changed(ObservableValue<? extends java.lang.Number> observable, Number oldValue, Number newValue) {
 127                 assertEquals("currentIndexProperty: old index is wrong", 2, oldValue);
 128                 assertEquals("currentIndexProperty: new index is wrong", 1, newValue);
 129                 observable.removeListener(this);
 130                 indexChanged.set(true);
 131             }
 132         });
 133 
 134         submit(() -> {
 135             // [1, 2*, 3]
 136             history.go(-1);
 137         });
 138         waitLoadFinished();
 139         check(new File("src/test/resources/test/html/h2.html"), 3, 1, "2");
 140 
 141         ensureValueChanged(dateChanged, "date not changed after go(-1)");
 142         ensureValueChanged(indexChanged, "index not changed after go(-1)");
 143 
 144         //
 145         // more go() checks
 146         //
 147 
 148         submit(() -> {
 149             // [1, 2, 3*]
 150             history.go(1);
 151         });
 152         waitLoadFinished();
 153         check(new File("src/test/resources/test/html/h3.html"), 3, 2, "3");
 154 
 155         submit(() -> {
 156             // [1*, 2, 3]
 157             history.go(-2);
 158         });
 159         waitLoadFinished();
 160         check(new File("src/test/resources/test/html/h1.html"), 3, 0, "1");
 161 
 162         submit(() -> {
 163             // [1*, 2, 3]
 164             history.go(0); // no-op
 165         });
 166 
 167         submit(() -> {
 168             // [1*, 2, 3]
 169             try {
 170                 history.go(-1);
 171                 fail("go: IndexOutOfBoundsException is not thrown");
 172             } catch (IndexOutOfBoundsException ex) {}
 173         });
 174 
 175         submit(() -> {
 176             // [1, 2, 3*]
 177             history.go(2);
 178         });
 179         waitLoadFinished();
 180         check(new File("src/test/resources/test/html/h3.html"), 3, 2, "3");
 181 
 182         submit(() -> {
 183             // [1, 2, 3*]
 184             try {
 185                 history.go(1);
 186                 fail("go: IndexOutOfBoundsException is not thrown");
 187             } catch (IndexOutOfBoundsException ex) {}
 188         });
 189 
 190         //
 191         // check the maxSize
 192         //
 193 
 194         submit(() -> {
 195             // [1, 2, 3*]
 196             history.setMaxSize(3);
 197         });
 198         // [2, 3, 1*]
 199         checkLoad(new File("src/test/resources/test/html/h1.html"), 3, 2, "1");
 200 
 201         submit(() -> {
 202             // [2, 3*]
 203             history.setMaxSize(2);
 204             assertEquals("entries: size is wrong", 2, history.getEntries().size());
 205             assertEquals("entries: title is wrong", "2", history.getEntries().get(0).getTitle());
 206         });
 207 
 208         submit(() -> {
 209             // [2, 3*]
 210             history.setMaxSize(3);
 211             // [2*, 3]
 212             history.go(-1);
 213         });
 214         waitLoadFinished();
 215 
 216         // [2, 1*]
 217         checkLoad(new File("src/test/resources/test/html/h1.html"), 2, 1, "1");
 218         // [2, 1, 3*]
 219         checkLoad(new File("src/test/resources/test/html/h3.html"), 3, 2, "3");
 220 
 221         submit(() -> {
 222             // [2*, 1, 3]
 223             history.go(-2);
 224         });
 225         waitLoadFinished();
 226 
 227         //
 228         // check for load in-beetwen
 229         //
 230 
 231         // [2, 3*]
 232         checkLoad(new File("src/test/resources/test/html/h3.html"), 2, 1, "3");
 233 
 234         //
 235         // check the date update on reload
 236         //
 237 
 238         // [2, 3, 4*]
 239         load(new File("src/test/resources/test/html/h4.html"));
 240 
 241         history.getEntries().get(history.getCurrentIndex()).lastVisitedDateProperty().addListener(newDateListener());
 242 
 243         try { Thread.sleep(150); } catch (Exception e) {} // ensure the next date doesn't fit into the same millisecond
 244 
 245         reload();
 246 
 247         ensureValueChanged(dateChanged, "date not changed after reload");
 248 
 249         //
 250         // finally, check zero and invalid maxSize
 251         //
 252 
 253         submit(() -> {
 254             // []
 255             history.setMaxSize(0);
 256             assertEquals("maxSizeProperty: wrong value", 0, history.getEntries().size());
 257 
 258             // []
 259             try {
 260                 history.maxSizeProperty().set(-1);
 261                 fail("maxSizeProperty: IllegalArgumentException is not thrown");
 262             } catch (IllegalArgumentException ex) {}
 263         });
 264     }
 265 
 266     /**
 267      * @test
 268      * @bug 8157686
 269      * Summary : Html history.pushState, history.back and history.replace
 270      * Ref : https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate
 271      */
 272     @Test(timeout = 30000) public void historyOnPopStateTest() throws Exception {
 273         final CountDownLatch latch = new CountDownLatch(1);
 274         final String page0 = "archive-root0.html";
 275         final String page1 = "archive-root1.html";
 276         final String page2 = "archive-root2.html";
 277         final String page3 = "longselectorlist.html";
 278 
 279         submit(() -> {
 280             WebEngine webEngine = new WebEngine();
 281             ClassLoader loader = LoadTest.class.getClassLoader();
 282 
 283             webEngine.locationProperty().addListener((observable, oldValue, newValue) -> {
 284                 if (oldValue == null) {
 285                     return;
 286                 }
 287 
 288                 if (oldValue.substring(oldValue.lastIndexOf('?') + 1).equalsIgnoreCase(page3) &&
 289                         newValue.substring(newValue.lastIndexOf('?') + 1).equalsIgnoreCase(page1)) {
 290                     webEngine.executeScript("history.back();");
 291                 } else if (oldValue.substring(oldValue.lastIndexOf('?') + 1).equalsIgnoreCase(page1) &&
 292                         newValue.substring(newValue.lastIndexOf('/') + 1).equalsIgnoreCase(page0)) {
 293                     assertNull("history.state should be null ", webEngine.executeScript("history.state"));
 294                     webEngine.executeScript("history.go(2);");
 295                 } else if(oldValue.substring(oldValue.lastIndexOf('/') + 1).equalsIgnoreCase(page0) &&
 296                         newValue.substring(newValue.lastIndexOf('?') + 1).equalsIgnoreCase(page3)) {
 297                     latch.countDown();
 298                 }
 299             });
 300 
 301             webEngine.getLoadWorker().stateProperty().addListener(((observable, oldValue, newValue) -> {
 302                 if (newValue == SUCCEEDED) {
 303                     webEngine.executeScript("history.pushState({page: 1}, 'title 1', '?" + page1 + "');");
 304                     assertEquals("history.pushState Failed",
 305                             page1,
 306                             webEngine.getLocation().substring(webEngine.getLocation().lastIndexOf('?') + 1)
 307                     );
 308 
 309                     webEngine.executeScript("history.pushState({page: 2}, 'title 2', '?" + page2 + "');");
 310                     assertEquals("history.pushState Failed",
 311                             page2,
 312                             webEngine.getLocation().substring(webEngine.getLocation().lastIndexOf('?') + 1)
 313                     );
 314 
 315                     webEngine.executeScript("history.replaceState({page: 3}, 'title 3', '?" + page3 + "');");
 316                     assertEquals("history.pushState Failed",
 317                             page3,
 318                             webEngine.getLocation().substring(webEngine.getLocation().lastIndexOf('?') + 1)
 319                     );
 320 
 321                     webEngine.executeScript("history.back();");
 322                 }
 323             }));
 324 
 325             webEngine.load(loader.getResource("test/html/archive-root0.html").toExternalForm());
 326         });
 327         try {
 328             latch.await();
 329         } catch (InterruptedException ex) {
 330             throw new AssertionError(ex);
 331         }
 332     }
 333 
 334     void checkLoad(File file, int size, int index, String title) {
 335         load(file);
 336         check(file, size, index, title);
 337     }
 338 
 339     void check(File file, int size, int index, String title) {
 340         assertEquals("entries: size is wrong", size, history.getEntries().size());
 341         assertEquals("currentIndex: index is wrong", index, history.getCurrentIndex());
 342         assertEquals("entries: url is wrong", file.toURI().toString(), history.getEntries().get(index).getUrl());
 343         assertEquals("entries: title is wrong", title, history.getEntries().get(index).getTitle());
 344     }
 345 
 346     void ensureValueChanged(AtomicBoolean value, String errMsg) {
 347         if (!value.compareAndSet(true, false)) {
 348             fail(errMsg);
 349         }
 350     }
 351 
 352     ChangeListener newDateListener() {
 353         return new ChangeListener<Date>() {
 354             long startTime = System.currentTimeMillis();
 355 
 356             public void changed(ObservableValue<? extends Date> observable, Date oldValue, Date newValue) {
 357                 long curTime = System.currentTimeMillis();
 358 
 359                 if (newValue.before(oldValue) ||
 360                     newValue.getTime() < startTime ||
 361                     newValue.getTime() > curTime)
 362                 {
 363                     System.out.println("oldValue=" + oldValue.getTime() +
 364                                        ", newValue=" + newValue.getTime() +
 365                                        ", startTime=" + startTime +
 366                                        ", curTime=" + curTime);
 367 
 368                     fail("entries: date is wrong");
 369                 }
 370                 observable.removeListener(this);
 371                 dateChanged.set(true);
 372             }
 373         };
 374 
 375     }
 376 }