1 /*
   2  * Copyright (c) 2011, 2018, 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;
  27 
  28 import javafx.collections.ObservableList;
  29 import com.sun.javafx.geom.BaseBounds;
  30 import com.sun.javafx.geom.transform.BaseTransform;
  31 import com.sun.javafx.sg.prism.NGGroup;
  32 import com.sun.javafx.sg.prism.NGNode;
  33 import javafx.scene.Group;
  34 import javafx.scene.Node;
  35 import javafx.scene.NodeShim;
  36 import javafx.scene.Parent;
  37 import javafx.scene.ParentShim;
  38 import javafx.scene.Scene;
  39 import org.junit.Rule;
  40 import org.junit.Test;
  41 import org.junit.rules.ExpectedException;
  42 import static org.junit.Assert.assertEquals;
  43 import static org.junit.Assert.assertNull;
  44 import static org.junit.Assert.assertSame;
  45 import static org.junit.Assert.assertTrue;
  46 import static org.junit.Assert.fail;
  47 import test.com.sun.javafx.scene.StubNodeHelper;
  48 import test.com.sun.javafx.scene.StubParentHelper;
  49 
  50 
  51 /**
  52  * Tests structural aspects of scene graph manipulation. See RT-4095.
  53  *
  54  * The following notation is used in test names to indicate various
  55  * relationships:
  56  *   CL = clip child
  57  *   G  = Group child
  58  *   S  = Scene child
  59  *
  60  * Several relationships are checked in each test, typically in
  61  * the following order:
  62  *
  63  * parent's clip
  64  * parent's children (for Group and Region)
  65  * relationships of second parent (if any)
  66  * child's clipParent
  67  * child's parent
  68  * child's scene
  69  */
  70 
  71 public class StructureTest {
  72     @Rule
  73     public ExpectedException thrown = ExpectedException.none();
  74 
  75     // TODO:
  76     //  - various nasty observableArrayList updates to Group.content and Scene.content.
  77     //  - various bind expressions.
  78 
  79     /////////////////////////
  80     // Setup and teardown. //
  81     /////////////////////////
  82 
  83     //////////////////////
  84     // Helper Functions //
  85     //////////////////////
  86 
  87     int occurs(Node child, ObservableList<Node> content) {
  88         int count = 0;
  89         if (content != null) {
  90             for (Node node : content) {
  91                 if (node == child) {
  92                     count++;
  93                 }
  94             }
  95         }
  96         return count;
  97     }
  98 
  99     /**
 100      * Checks whether child occurs exactly once in the content observableArrayList.
 101      */
 102     boolean occursOnce(Node child, ObservableList<Node> content) {
 103         return 1 == occurs(child, content);
 104     }
 105 
 106     /**
 107      * Checks whether child does not occur in the content observableArrayList.
 108      */
 109     boolean occursZero(Node child, ObservableList<Node> content) {
 110         return 0 == occurs(child, content);
 111     }
 112 
 113     /**
 114      * Checks whether the child node appears exactly once in the
 115      * Group's content observableArrayList.
 116      */
 117     boolean isChild(Node child, Group group) {
 118         return occursOnce(child, ParentShim.getChildren(group));
 119     }
 120 
 121     /**
 122      * Checks whether the child node is the root of the Scene.
 123      */
 124     boolean isRoot(Parent root, Scene scene) {
 125         return root == scene.getRoot();
 126     }
 127 
 128     /**
 129      * Checks whether the child node does not appear in the
 130      * Group's content observableArrayList.
 131      *
 132      * We use assertTrue(notChild(child, parent)) instead of
 133      * assertFalse(isChild(child, parent)) because we want
 134      * to catch cases where child occurs more than once in
 135      * the parent's content observableArrayList.
 136      */
 137     boolean notChild(Node child, Group group) {
 138         return occursZero(child, ParentShim.getChildren(group));
 139     }
 140 
 141     /**
 142      * Checks whether the child node does not appear as the root of the
 143      * Scene.
 144      */
 145     boolean notRoot(Parent root, Scene scene) {
 146         return root != scene.getRoot();
 147     }
 148 
 149     /////////////////////////////////////
 150     // Simple Structural Relationships //
 151     /////////////////////////////////////
 152 
 153     @Test
 154     public void testOrphan() {
 155         Node n = new StubNode();
 156 
 157         assertNull("clipParent is null", NodeShim.getClipParent(n));
 158         assertNull("parent is null", n.getParent());
 159         assertNull("scene is null", n.getScene());
 160     }
 161 
 162     @Test
 163     public void testSimpleCL() {
 164         StubNode parent = new StubNode();
 165         StubNode child = new StubNode();
 166         parent.setClip(child);
 167 
 168         assertSame("parent.clip is child", child, parent.getClip());
 169         assertSame("child.clipParent is parent", parent, NodeShim.getClipParent(child));
 170         assertNull("child.parent is null", child.getParent());
 171         assertNull("scene is null", child.getScene());
 172     }
 173 
 174     @Test
 175     public void testSimpleG() {
 176         StubNode child = new StubNode();
 177         Group group = new Group(child);
 178 
 179         assertNull("group.clip is null", group.getClip());
 180         assertTrue("isChild of group", isChild(child, group));
 181         assertNull("child.clipParent is null", NodeShim.getClipParent(child));
 182         assertSame("child.parent is parent", group, child.getParent());
 183         assertNull("child.getScene() is null", child.getScene());
 184     }
 185 
 186     @Test
 187     public void testSimpleS() {
 188         StubParent root = new StubParent();
 189         Scene scene = new Scene(root);
 190 
 191         assertTrue("isChild of scene", isRoot(root, scene));
 192         assertNull("child.clipParent is null", NodeShim.getClipParent(root));
 193         assertSame("child.getScene() is scene", scene, root.getScene());
 194     }
 195 
 196     @Test
 197     public void testSceneInsertGroup1() {
 198         StubNode child = new StubNode();
 199         Group group = new Group();
 200         Scene scene = new Scene(group);
 201         ParentShim.getChildren(group).add(child);
 202 
 203         assertSame("group.getScene() is scene", scene, group.getScene());
 204         assertSame("child.getScene() is scene", scene, child.getScene());
 205     }
 206 
 207     @Test
 208     public void testSceneInsertGroup2() {
 209         StubNode child = new StubNode();
 210         Group group = new Group();
 211         Scene scene = new Scene(group);
 212         ParentShim.getChildren(group).add(child);
 213 
 214 
 215         assertSame("group.getScene() is scene", scene, group.getScene());
 216         assertSame("child.getScene() is scene", scene, child.getScene());
 217     }
 218 
 219     @Test public void testUnparentCL() {
 220         StubNode child = new StubNode();
 221         StubNode parent = new StubNode();
 222         parent.setClip(child);
 223         parent.setClip(null);
 224 
 225         assertNull("parent.clip is null", parent.getClip());
 226         assertNull("child.clipParent is null", NodeShim.getClipParent(child));
 227     }
 228 
 229     @Test public void testUnparentG() {
 230         StubNode child = new StubNode();
 231         Group parent = new Group(child);
 232 
 233         ParentShim.getChildren(parent).remove(child);
 234 
 235 
 236         assertEquals("parent.content is zero size", 0, ParentShim.getChildren(parent).size());
 237         assertNull("child.parent is null", child.getParent());
 238     }
 239 
 240     ////////////////////////////////////
 241     // Illegal Structure Change Tests //
 242     ////////////////////////////////////
 243 
 244     // Test attempts to switch from one part of the scene graph to another.
 245     // This is the cross product: {CL,CU,G,S}x{CL,CU,G,S} so there
 246     // are sixteen cases.
 247 
 248     @Test public void testSwitchCLCL() {
 249         StubNode child = new StubNode();
 250         StubNode p1 = new StubNode();
 251         p1.setClip(child);
 252         StubNode p2 = new StubNode();
 253         thrown.expect(IllegalArgumentException.class);
 254         try {
 255             p2.setClip(child);
 256         } catch (final IllegalArgumentException e) {
 257             assertSame("p1.clip is child", child, p1.getClip());
 258             assertNull("p2.clip is null", p2.getClip());
 259             assertSame("child.clipParent is p1",
 260                        p1, NodeShim.getClipParent(child));
 261             assertNull("child.parent is null", child.getParent());
 262             assertNull("child.getScene() is null", child.getScene());
 263             throw e;
 264         }
 265     }
 266 
 267     @Test public void testSwitchCLG() {
 268         StubNode child = new StubNode();
 269         StubNode p1 = new StubNode();
 270         p1.setClip(child);
 271         Group p2 = new Group();
 272         ObservableList<Node> content = ParentShim.getChildren(p2);
 273         try {
 274             content.add(child);
 275             fail("IllegalArgument should have been thrown.");
 276         } catch (IllegalArgumentException iae) {
 277             // expected
 278         }
 279 
 280         assertSame("p1.clip is child", child, p1.getClip());
 281         assertNull("p2.clip is null", p2.getClip());
 282         assertTrue("notChild of p2", notChild(child, p2));
 283         assertSame("child.clipParent is p1", p1, NodeShim.getClipParent(child));
 284         assertNull("child.parent is null", child.getParent());
 285         assertNull("child.getScene() is null", child.getScene());
 286     }
 287 
 288     @Test public void testSwitchCLS() {
 289         StubParent clipNode = new StubParent();
 290         StubNode p1 = new StubNode();
 291         p1.setClip(clipNode);
 292         try {
 293             Scene p2 = new Scene(clipNode);
 294             fail("IllegalArgument should have been thrown.");
 295         } catch (Throwable t) {
 296             //expected
 297         }
 298         assertSame("p1.clip is child", clipNode, p1.getClip());
 299         assertSame("child.clipParent is p1", p1, NodeShim.getClipParent(clipNode));
 300         assertNull("child.parent is null", clipNode.getParent());
 301         assertNull("child.getScene() is null", clipNode.getScene());
 302     }
 303 
 304     @Test public void testSwitchGCL() {
 305         StubNode child = new StubNode();
 306         Group p1 = new Group(child);
 307         StubNode p2 = new StubNode();
 308         thrown.expect(IllegalArgumentException.class);
 309         try {
 310             p2.setClip(child);
 311         } catch (final IllegalArgumentException e) {
 312             assertNull("p1.clip is null", p1.getClip());
 313             assertTrue("isChild of p1", isChild(child, p1));
 314             assertNull("p2.clip is null", p2.getClip());
 315             assertNull("child.clipParent is null", NodeShim.getClipParent(child));
 316             assertSame("child.parent is p1", p1, child.getParent());
 317             assertNull("child.getScene() is null", child.getScene());
 318             throw e;
 319         }
 320     }
 321 
 322 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 323 
 324 //    @Test public void testSwitchGG() {
 325 //        var child = new StubNode();
 326 //        var p1 = Group { content: [ child ] };
 327 //        setHandler();
 328 //        var p2 = Group { content: [ child ] };
 329 //
 330 //        assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException);
 331 //        assertNull("p1.clip is null", p1.clip);
 332 //        assertTrue("isChild of p1", isChild(child, p1));
 333 //        assertNull("p2.clip is null", p2.clip);
 334 //        assertTrue("notChild of p2", notChild(child, p2));
 335 //        assertNull("child.clipParent is null", NodeShim.getClipParent(child));
 336 //        assertSame("child.parent is p1", p1, child.parent);
 337 //        assertNull("child.getScene() is null", child.getScene());
 338 //    }
 339 
 340 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 341 
 342 //    @Test public void testSwitchGS() {
 343 //        var child = new StubNode();
 344 //        var p1 = Group { content: [ child ] };
 345 //        setHandler();
 346 //        var p2 = Scene { content: [ child ] };
 347 //
 348 //        assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException);
 349 //        assertNull("p1.clip is null", p1.clip);
 350 //        assertTrue("isChild of p1", isChild(child, p1));
 351 //        assertTrue("notChild of p2", notChild(child, p2));
 352 //        assertNull("child.clipParent is null", NodeShim.getClipParent(child));
 353 //        assertSame("child.parent is p1", p1, child.parent);
 354 //        assertNull("child.getScene() is null", child.getScene());
 355 //    }
 356 
 357 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST OF STOPGAP POLICY
 358 
 359     @Test public void testSwitchGGStopgap() {
 360         StubNode child = new StubNode();
 361         Group p1 = new Group(child);
 362         Group p2 = new Group(child);
 363 
 364         assertTrue("notChild of p1", notChild(child, p1));
 365         assertTrue("isChild of p2", isChild(child, p2));
 366         assertSame("child.parent is p2", p2, child.getParent());
 367     }
 368 
 369     @Test public void testSwitchSCL() {
 370         StubParent root = new StubParent();
 371         Scene scene = new Scene(root);
 372         StubNode p2 = new StubNode();
 373         thrown.expect(IllegalArgumentException.class);
 374         try {
 375             p2.setClip(root);
 376         } catch (final IllegalArgumentException e) {
 377             assertTrue("isRoot of scene", isRoot(root, scene));
 378             assertNull("p2.clip is null", p2.getClip());
 379             assertNull("root.clipParent is null", NodeShim.getClipParent(root));
 380             assertSame("root.getScene() is scene", scene, root.getScene());
 381             throw e;
 382         }
 383     }
 384 
 385 
 386 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 387 
 388 //    @Test public void testSwitchSG() {
 389 //        var child = new StubNode();
 390 //        var p1 = Scene { content: [ child ] };
 391 //        setHandler();
 392 //        var p2 = Group { content: [ child ] };
 393 //
 394 //        assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException);
 395 //        assertTrue("isChild of p1", isChild(child, p1));
 396 //        assertNull("p2.clip is null", p2.clip);
 397 //        assertTrue("notChild of p2", notChild(child, p2));
 398 //        assertNull("child.clipParent is null", NodeShim.getClipParent(child));
 399 //        assertSame("child.parent is p1.impl_root", p1.impl_root, child.parent);
 400 //        assertSame("child.getScene() is p1", p1, child.getScene());
 401 //    }
 402 
 403 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 404 
 405 //    @Test public void testSwitchSS() {
 406 //        var child = new StubNode();
 407 //        var p1 = Scene { content: [ child ] };
 408 //        setHandler();
 409 //        var p2 = Scene { content: [ child ] };
 410 //
 411 //        assertTrue("isChild of p1", isChild(child, p1));
 412 //        assertTrue("notChild of p2", notChild(child, p2));
 413 //        assertNull("child.clipParent is null", NodeShim.getClipParent(child));
 414 //        assertSame("child.parent is p1.impl_root", p1.impl_root, child.parent);
 415 //        assertSame("child.getScene() is p1", p1, child.getScene());
 416 //    }
 417 
 418     @Test public void testGroupInsert() {
 419         StubNode n0 = new StubNode();
 420         n0.setId("n0");
 421         StubNode n1 = new StubNode();
 422         n1.setId("n1");
 423         StubNode n2 = new StubNode();
 424         n2.setId("n2");
 425         Group g = new Group(n0, n1, n2);
 426 
 427         ObservableList<Node> content = ParentShim.getChildren(g);
 428         try {
 429             content.add(n1);
 430             fail("IllegalArgument should have been thrown.");
 431         } catch (IllegalArgumentException iae) {
 432             // expected
 433         }
 434 
 435         assertEquals("g.content is size 3", 3, ParentShim.getChildren(g).size());
 436         assertSame("g.content[0] is n0", n0, ParentShim.getChildren(g).get(0));
 437         assertSame("g.content[1] is n1", n1, ParentShim.getChildren(g).get(1));
 438         assertSame("g.content[2] is n2", n2, ParentShim.getChildren(g).get(2));
 439 
 440     }
 441 
 442 
 443     @Test public void testGroupReplace1() {
 444         StubNode n0 = new StubNode();
 445         n0.setId("n0");
 446         StubNode n1 = new StubNode();
 447         n1.setId("n1");
 448         StubNode n2 = new StubNode();
 449         n2.setId("n2");
 450         Group g = new Group(n0, n1, n2);
 451 
 452         ParentShim.getChildren(g).remove(1);
 453         ObservableList<Node> n = javafx.collections.FXCollections.<Node>observableArrayList();
 454         n.addAll(n1,n1);
 455         ObservableList<Node> content = ParentShim.getChildren(g);
 456         try {
 457             content.addAll(1, n);
 458             fail("IllegalArgument should have been thrown.");
 459         } catch (IllegalArgumentException iae) {
 460             // expected
 461         }
 462 
 463         assertEquals("g.content is size 2", 2, ParentShim.getChildren(g).size());
 464         assertSame("g.content[0] is n0", n0, ParentShim.getChildren(g).get(0));
 465         assertSame("g.content[1] is n2", n2, ParentShim.getChildren(g).get(1));
 466     }
 467 
 468     @Test public void testGroupReplace2() {
 469         StubNode n0 = new StubNode();
 470         n0.setId("n0");
 471         StubNode n1 = new StubNode();
 472         n1.setId("n1");
 473         StubNode n2 = new StubNode();
 474         n2.setId("n2");
 475         Group g = new Group(n0, n1, n2);
 476 
 477         try {
 478             ParentShim.getChildren(g).set(1, n0);
 479             fail("No exception thrown.");
 480         } catch (IllegalArgumentException e) {
 481             //Expected
 482         }
 483 
 484         assertEquals("g.content is size 3", 3, ParentShim.getChildren(g).size());
 485         assertSame("g.content[0] is n0", n0, ParentShim.getChildren(g).get(0));
 486         assertSame("g.content[1] is n1", n1, ParentShim.getChildren(g).get(1));
 487         assertSame("g.content[2] is n2", n2, ParentShim.getChildren(g).get(2));
 488     }
 489 
 490     @Test public void testGroupReplace3() {
 491         StubNode n0 = new StubNode();
 492         n0.setId("n0");
 493         StubNode n1 = new StubNode();
 494         n1.setId("n1");
 495         StubNode n2 = new StubNode();
 496         n2.setId("n2");
 497         Group g = new Group(n0, n1, n2);
 498         ParentShim.getChildren(g).set(1, n1);
 499         ParentShim.getChildren(g).set(2, n2);
 500 
 501         assertEquals("g.content is size 3", 3, ParentShim.getChildren(g).size());
 502         assertSame("g.content[0] is n0", n0, ParentShim.getChildren(g).get(0));
 503         assertSame("g.content[1] is n1", n1, ParentShim.getChildren(g).get(1));
 504         assertSame("g.content[2] is n2", n2, ParentShim.getChildren(g).get(2));
 505     }
 506 
 507     ///////////////////////
 508     // Circularity Tests //
 509     ///////////////////////
 510 
 511     // General form is: given an existing relationship of one kind, add
 512     // another relationship of some kind that would cause a circularity.
 513     // This is the cross product: {CL,CU,G}x{CL,CU,G}. Also test degenerate
 514     // circularities where a node is its own clip, its own child.
 515     // The Scene relationship does not occur here, because a Scene cannot
 516     // participate in any circular relationship as it has no parent.
 517 
 518     // Test only {CL,G}x{CL,G} for now.
 519 
 520     @Test public void testCircularCLCL() {
 521         StubNode node1 = new StubNode();
 522         StubNode node2 = new StubNode();
 523         node2.setClip(node1);
 524         thrown.expect(IllegalArgumentException.class);
 525         try {
 526             node1.setClip(node2);
 527         } catch (final IllegalArgumentException e) {
 528             assertNull("node1.clip is null", node1.getClip());
 529             assertSame("node1.clipParent is node2",
 530                        node2,
 531                        NodeShim.getClipParent(node1));
 532             assertSame("node2.clip is node1", node1, node2.getClip());
 533             assertNull("node2.clipParent is null", NodeShim.getClipParent(node2));
 534             throw e;
 535         }
 536     }
 537 
 538     @Test public void testCircularCLG() {
 539         StubNode node1 = new StubNode();
 540         Group node2 = new Group(node1);
 541         thrown.expect(IllegalArgumentException.class);
 542         try {
 543             node1.setClip(node2);
 544         } catch (final IllegalArgumentException e) {
 545             assertNull("node1.clip is null", node1.getClip());
 546             assertNull("node1.clipParent is null", NodeShim.getClipParent(node1));
 547             assertSame("node1.parent is node2", node2, node1.getParent());
 548             assertNull("node2.clip is null", node2.getClip());
 549             assertNull("node2.clipParent is null", NodeShim.getClipParent(node2));
 550             assertTrue("node1 is child of node2", isChild(node1, node2));
 551             throw e;
 552         }
 553     }
 554 
 555     @Test public void testCircularGCL() {
 556         Group node1 = new Group();
 557         StubNode node2 = new StubNode();
 558         node2.setClip(node1);
 559 
 560         ObservableList<Node> content = ParentShim.getChildren(node1);
 561         try {
 562             content.add(node2);
 563             fail("IllegalArgument should have been thrown.");
 564         } catch (IllegalArgumentException iae) {
 565             // expected
 566         }
 567 
 568         assertNull("node1.clip is null", node1.getClip());
 569         assertSame("node1.clipParent is node2", node2, NodeShim.getClipParent(node1));
 570         assertTrue("node2 is not child of node1", notChild(node2, node1));
 571         assertSame("node2.clip is node1", node1, node2.getClip());
 572         assertNull("node2.clipParent is null", NodeShim.getClipParent(node2));
 573         assertNull("node2.parent is null", node2.getParent());
 574     }
 575 
 576     @Test public void testCircularGG() {
 577         Group node1 = new Group();
 578         Group node2 = new Group(node1);
 579 
 580         ObservableList<Node> content = ParentShim.getChildren(node1);
 581         try {
 582             content.add(node2);
 583             fail("IllegalArgument should have been thrown.");
 584         } catch (IllegalArgumentException iae) {
 585             // expected
 586         }
 587 
 588         assertSame("node1.parent is node2", node2, node1.getParent());
 589         assertTrue("node2 is not a child of node1", notChild(node2, node1));
 590         assertNull("node2.parent is null", node2.getParent());
 591         assertTrue("node1 is child of node2", isChild(node1, node2));
 592     }
 593 
 594     @Test public void testCircularSelfCL() {
 595         StubNode node1 = new StubNode();
 596         thrown.expect(IllegalArgumentException.class);
 597         try {
 598             node1.setClip(node1);
 599         } catch (final IllegalArgumentException e) {
 600             assertNull("node1.clip is null", node1.getClip());
 601             assertNull("node1.clipParent is null", NodeShim.getClipParent(node1));
 602             throw e;
 603         }
 604     }
 605 
 606     @Test public void testCircularSelfG() {
 607         Group node1 = new Group();
 608 
 609         ObservableList<Node> content = ParentShim.getChildren(node1);
 610         try {
 611             content.add(node1);
 612             fail("IllegalArgument should have been thrown.");
 613         } catch (IllegalArgumentException iae) {
 614             // expected
 615         }
 616 
 617         assertTrue("node1 is not a child of itself", notChild(node1, node1));
 618         assertNull("node1.parent is null", node1.getParent());
 619     }
 620 
 621     //////////////////////////
 622     // Bound Variable Tests //
 623     //////////////////////////
 624 
 625     // Test various cases where a structure variable (Node.clip,
 626     // Group.content, Scene.content) is initialized to a bind-expression.
 627     // If the trigger attempts to roll back a change to a variable
 628     // initialized this way, the attempt will fail and will throw an
 629     // exception. This will leave the invariant violation in place!
 630     // We can't do anything about this without language support, so
 631     // don't test these cases for now.
 632 
 633 // FAILS:
 634 //    @Test public void testBindClip() {
 635 //        var c:Node = null;
 636 //        var p1 = StubNode { clip: bind c id: "p1" };
 637 //        var p2 = StubNode { clip: bind c id: "p2" };
 638 //        c = StubNode { id: "c" };
 639 //
 640 //        println("testBindClip");
 641 //        println("p1 = {p1}");
 642 //        println("p2 = {p2}");
 643 //        println("c = {c}");
 644 //        println("p1.clip = {p1.clip}");
 645 //        println("p2.clip = {p2.clip}");
 646 //        println("c.clipParent = {c.getClipParent()}");
 647 //    }
 648 
 649     ////////////////////
 650     // Helper Classes //
 651     ////////////////////
 652 
 653     //
 654     // * A stub node that contains as little functionality as possible.
 655     // *
 656     public static final class StubNode extends Node {
 657         static {
 658             StubNodeHelper.setStubNodeAccessor(new StubNodeHelper.StubNodeAccessor() {
 659                 @Override
 660                 public NGNode doCreatePeer(Node node) {
 661                     return ((StubNode) node).doCreatePeer();
 662                 }
 663                 @Override
 664                 public BaseBounds doComputeGeomBounds(Node node,
 665                         BaseBounds bounds, BaseTransform tx) {
 666                     return ((StubNode) node).doComputeGeomBounds(bounds, tx);
 667                 }
 668                 @Override
 669                 public boolean doComputeContains(Node node, double localX, double localY) {
 670                     return ((StubNode) node).doComputeContains(localX, localY);
 671                 }
 672             });
 673         }
 674 
 675         {
 676             // To initialize the class helper at the begining each constructor of this class
 677             StubNodeHelper.initHelper(this);
 678         }
 679         public StubNode() {
 680             super();
 681         }
 682 
 683         // * Returning null causes crashes so return a NGGroup.
 684         private NGNode doCreatePeer() {
 685             return new NGGroup();
 686         }
 687 
 688         private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 689             return bounds;
 690         }
 691 
 692         /*
 693          * Note: This method MUST only be called via its accessor method.
 694          */
 695         private boolean doComputeContains(double localX, double localY) {
 696             // TODO: Missing code.
 697             return false;
 698         }
 699     }
 700 
 701     public static final class StubParent extends Parent {
 702         static {
 703             StubParentHelper.setStubParentAccessor(new StubParentHelper.StubParentAccessor() {
 704                 @Override
 705                 public NGNode doCreatePeer(Node node) {
 706                     return ((StubParent) node).doCreatePeer();
 707                 }
 708 
 709                 @Override
 710                 public BaseBounds doComputeGeomBounds(Node node,
 711                         BaseBounds bounds, BaseTransform tx) {
 712                     return ((StubParent) node).doComputeGeomBounds(bounds, tx);
 713                 }
 714 
 715                 @Override
 716                 public boolean doComputeContains(Node node, double localX, double localY) {
 717                     return ((StubParent) node).doComputeContains(localX, localY);
 718                 }
 719             });
 720         }
 721 
 722         {
 723             // To initialize the class helper at the begining each constructor of this class
 724             StubParentHelper.initHelper(this);
 725         }
 726         public StubParent() {
 727             super();
 728         }
 729 
 730         // * Returning null causes crashes so return a PGGroup.
 731         private NGNode doCreatePeer() {
 732             return new NGGroup();
 733         }
 734 
 735         private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 736             return bounds;
 737         }
 738 
 739         /*
 740          * Note: This method MUST only be called via its accessor method.
 741          */
 742         private boolean doComputeContains(double localX, double localY) {
 743             // TODO: Missing code.
 744             return false;
 745         }
 746     }
 747 
 748 }