Repository: sjbarag/brs
Pull request #264 (2019-06-19 16:58:27)

Implement if sg node children
This PR implements 6 of the most used functions in `ifSGNodeChildren` interface:
- appendChild()
- getChildCount()
- getChildren()
- removeChild()
- getParent()
- createChild()


Patch:
diff --git a/src/brsTypes/components/RoSGNode.ts b/src/brsTypes/components/RoSGNode.ts
index 9848d0d..7a5991a 100644
--- a/src/brsTypes/components/RoSGNode.ts
+++ b/src/brsTypes/components/RoSGNode.ts
@@ -16,6 +16,8 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
     readonly kind = ValueKind.Object;
     elements = new Map<string, BrsType>();
     private fields = new Map<string, Field>();
+    private children: RoSGNode[] = [];
+    private parent: RoSGNode | BrsInvalid = BrsInvalid.Instance;
 
     constructor(elements: AAMember[], readonly type: string = "Node") {
         super("roSGNode");
@@ -34,6 +36,13 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
             this.keys,
             this.items,
             this.lookup,
+            //ifSGNodeChildren
+            this.appendchild,
+            this.getchildcount,
+            this.getchildren,
+            this.removechild,
+            this.getparent,
+            this.createchild,
         ]);
     }
 
@@ -102,6 +111,14 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
         return BrsInvalid.Instance;
     }
 
+    setParent(parent: RoSGNode) {
+        this.parent = parent;
+    }
+
+    removeParent() {
+        this.parent = BrsInvalid.Instance;
+    }
+
     /** Removes all elements from the node */
     private clear = new Callable("clear", {
         signature: {
@@ -213,6 +230,120 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
             return this.get(new BrsString(lKey));
         },
     });
+
+    /* Return the current number of children in the subject node list of children.
+    This is always a non-negative number. */
+    private getchildcount = new Callable("getchildcount", {
+        signature: {
+            args: [],
+            returns: ValueKind.Int32,
+        },
+        impl: (interpreter: Interpreter) => {
+            return new Int32(this.children.length);
+        },
+    });
+
+    /* Adds a child node to the end of the subject node list of children so that it is
+    traversed last (of those children) during render. */
+    private appendchild = new Callable("appendchild", {
+        signature: {
+            args: [new StdlibArgument("child", ValueKind.Dynamic)],
+            returns: ValueKind.Boolean,
+        },
+        impl: (interpreter: Interpreter, child: BrsType) => {
+            if (child instanceof RoSGNode) {
+                if (this.children.includes(child)) {
+                    return BrsBoolean.True;
+                }
+                this.children.push(child);
+                child.setParent(this);
+                return BrsBoolean.True;
+            }
+            return BrsBoolean.False;
+        },
+    });
+
+    /* Retrieves the number of child nodes specified by num_children from the subject
+    node, starting at the position specified by index. Returns an array of the child nodes
+    retrieved. If num_children is -1, return all the children. */
+    private getchildren = new Callable("getchildren", {
+        signature: {
+            args: [
+                new StdlibArgument("num_children", ValueKind.Int32),
+                new StdlibArgument("index", ValueKind.Int32),
+            ],
+            returns: ValueKind.Object,
+        },
+        impl: (interpreter: Interpreter, num_children: Int32, index: Int32) => {
+            let numChildrenValue = num_children.getValue();
+            let indexValue = index.getValue();
+            let childrenSize = this.children.length;
+            if (numChildrenValue <= -1 && indexValue === 0) {
+                //short hand to return all children
+                return new RoArray(this.children.slice());
+            } else if (numChildrenValue <= 0 || indexValue < 0 || indexValue >= childrenSize) {
+                //these never return any children
+                return new RoArray([]);
+            } else {
+                //only valid cases
+                return new RoArray(this.children.slice(indexValue, indexValue + numChildrenValue));
+            }
+
+            return new RoArray([]);
+        },
+    });
+
+    /* Finds a child node in the subject node list of children, and if found,
+    remove it from the list of children. The match is made on the basis of actual
+    object identity, that is, the value of the pointer to the child node.
+    return false if trying to remove anything that's not a node */
+    private removechild = new Callable("removechild", {
+        signature: {
+            args: [new StdlibArgument("child", ValueKind.Dynamic)],
+            returns: ValueKind.Boolean,
+        },
+        impl: (interpreter: Interpreter, child: BrsType) => {
+            if (child instanceof RoSGNode) {
+                let spliceIndex = this.children.indexOf(child);
+                if (spliceIndex >= 0) {
+                    child.removeParent();
+                    this.children.splice(spliceIndex, 1);
+                }
+                return BrsBoolean.True;
+            }
+            return BrsBoolean.False;
+        },
+    });
+    /* If the subject node has been added to a parent node list of children,
+    return the parent node, otherwise return invalid.*/
+    private getparent = new Callable("getparent", {
+        signature: {
+            args: [],
+            returns: ValueKind.Dynamic,
+        },
+        impl: (interpreter: Interpreter) => {
+            return this.parent;
+        },
+    });
+
+    /* Creates a child node of type nodeType, and adds the new node to the end of the
+    subject node list of children */
+    private createchild = new Callable("createchild", {
+        signature: {
+            args: [new StdlibArgument("nodetype", ValueKind.String)],
+            returns: ValueKind.Object,
+        },
+        impl: (interpreter: Interpreter, nodetype: BrsString) => {
+            // currently we can't create a custom subclass object of roSGNode,
+            // so we'll always create generic RoSGNode object as child
+            let child = createNodeByType(nodetype);
+            if (child instanceof RoSGNode) {
+                this.children.push(child);
+                child.setParent(this);
+            }
+            return child;
+        },
+    });
 }
 
 export function createNodeByType(type: BrsString) {


Test patch:
diff --git a/test/brsTypes/components/RoSGNode.test.js b/test/brsTypes/components/RoSGNode.test.js
index 4f8d651..ea36ea4 100644
--- a/test/brsTypes/components/RoSGNode.test.js
+++ b/test/brsTypes/components/RoSGNode.test.js
@@ -278,4 +278,304 @@ describe("RoSGNode", () => {
             });
         });
     });
+
+    describe("ifSGNodeChildren", () => {
+        let interpreter, parent, child1, child2, child3, child4;
+
+        beforeEach(() => {
+            interpreter = new Interpreter();
+            parent = new RoSGNode([{ name: new BrsString("parent"), value: new BrsString("1") }]);
+            child1 = new RoSGNode([{ name: new BrsString("child"), value: new BrsString("2") }]);
+            child2 = new RoSGNode([{ name: new BrsString("child"), value: new BrsString("3") }]);
+            child3 = new RoSGNode([{ name: new BrsString("child"), value: new BrsString("4") }]);
+            child4 = new RoSGNode([{ name: new BrsString("child"), value: new BrsString("5") }]);
+        });
+
+        describe("getchildcount", () => {
+            it("returns the number of children", () => {
+                let getChildCount = parent.getMethod("getchildcount");
+                expect(getChildCount).toBeTruthy();
+
+                let result = getChildCount.call(interpreter);
+                expect(result).toEqual(new Int32(0));
+            });
+        });
+
+        describe("appendchild", () => {
+            it("appends a child to the node", () => {
+                let appendChild = parent.getMethod("appendchild");
+                let getChildCount = parent.getMethod("getchildcount");
+
+                let result = appendChild.call(interpreter, child1);
+                let childCount = getChildCount.call(interpreter);
+                expect(result).toEqual(BrsBoolean.True);
+                expect(appendChild).toBeTruthy();
+                expect(childCount).toEqual(new Int32(1));
+            });
+
+            it("appends invalid to the node", () => {
+                let invalidChild = BrsInvalid.Instance;
+
+                let appendChild = parent.getMethod("appendchild");
+                let getChildCount = parent.getMethod("getchildcount");
+
+                let result = appendChild.call(interpreter, invalidChild);
+                let childCount = getChildCount.call(interpreter);
+                expect(result).toEqual(BrsBoolean.False);
+                expect(childCount).toEqual(new Int32(0));
+            });
+
+            it("doesn't append the same node twice", () => {
+                let appendChild = parent.getMethod("appendchild");
+                let getChildCount = parent.getMethod("getchildcount");
+
+                let result = appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child1);
+                let childCount = getChildCount.call(interpreter);
+                expect(result).toEqual(BrsBoolean.True);
+                expect(childCount).toEqual(new Int32(1));
+            });
+        });
+
+        describe("getchildren", () => {
+            it("get children of node without children", () => {
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+
+                let result = getChildren.call(interpreter, new Int32(-1), new Int32(0));
+                let childCount = getChildCount.call(interpreter);
+                expect(getChildren).toBeTruthy();
+                expect(childCount).toEqual(new Int32(0));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual([]);
+            });
+
+            it("get all children of node with children", () => {
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+
+                let result = getChildren.call(interpreter, new Int32(-1), new Int32(0));
+                let childCount = getChildCount.call(interpreter);
+                expect(childCount).toEqual(new Int32(3));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual([child1, child2, child3]);
+            });
+
+            it("get last two children", () => {
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+                appendChild.call(interpreter, child4);
+
+                // get last 2 children
+                let result = getChildren.call(interpreter, new Int32(2), new Int32(2));
+                let childCount = getChildCount.call(interpreter);
+                expect(childCount).toEqual(new Int32(4));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual([child3, child4]);
+            });
+
+            it("pass in large child count", () => {
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+                appendChild.call(interpreter, child4);
+
+                // pass in more children count than there are children, should just return 2 with offset of 2
+                let result1 = getChildren.call(interpreter, new Int32(20), new Int32(2));
+                expect(result1).toBeInstanceOf(RoArray);
+                expect(result1.elements).toEqual([child3, child4]);
+            });
+
+            it("pass in negative indices", () => {
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+                appendChild.call(interpreter, child4);
+
+                // pass in negative indices
+                let result2 = getChildren.call(interpreter, new Int32(-10), new Int32(-50));
+                expect(result2).toBeInstanceOf(RoArray);
+                expect(result2.elements).toEqual([]);
+            });
+
+            it("pass in negative start index", () => {
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+                appendChild.call(interpreter, child4);
+
+                // pass in just negative start index
+                let result3 = getChildren.call(interpreter, new Int32(5), new Int32(-50));
+                expect(result3).toBeInstanceOf(RoArray);
+                expect(result3.elements).toEqual([]);
+            });
+
+            it("get first 3 children", () => {
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+                appendChild.call(interpreter, child4);
+
+                // get first 3 children
+                let result4 = getChildren.call(interpreter, new Int32(3), new Int32(0));
+                expect(result4).toBeInstanceOf(RoArray);
+                expect(result4.elements).toEqual([child1, child2, child3]);
+            });
+        });
+
+        describe("removechild", () => {
+            it("remove a child node from parent", () => {
+                let removeChild = parent.getMethod("removechild");
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+                expect(removeChild).toBeTruthy();
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+
+                // remove child 2
+                removeChild.call(interpreter, child2);
+                let result = getChildren.call(interpreter, new Int32(-1), new Int32(0));
+                let childCount = getChildCount.call(interpreter);
+                expect(childCount).toEqual(new Int32(2));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual([child1, child3]);
+
+                // remove child 3
+                removeChild.call(interpreter, child3);
+                let result1 = getChildren.call(interpreter, new Int32(-1), new Int32(0));
+                let childCount1 = getChildCount.call(interpreter);
+                expect(childCount1).toEqual(new Int32(1));
+                expect(result1).toBeInstanceOf(RoArray);
+                expect(result1.elements).toEqual([child1]);
+
+                // remove child 1
+                removeChild.call(interpreter, child1);
+                let result2 = getChildren.call(interpreter, new Int32(-1), new Int32(0));
+                let childCount2 = getChildCount.call(interpreter);
+                expect(childCount2).toEqual(new Int32(0));
+                expect(result2).toBeInstanceOf(RoArray);
+                expect(result2.elements).toEqual([]);
+
+                // try to remove child 1 again, but shouldn't do anything
+                removeChild.call(interpreter, child1);
+                let result3 = getChildren.call(interpreter, new Int32(-1), new Int32(0));
+                let childCount3 = getChildCount.call(interpreter);
+                expect(childCount3).toEqual(new Int32(0));
+                expect(result3).toBeInstanceOf(RoArray);
+                expect(result3.elements).toEqual([]);
+            });
+
+            it("remove non-node things from parent", () => {
+                let removeChild = parent.getMethod("removechild");
+                let getChildren = parent.getMethod("getchildren");
+                let getChildCount = parent.getMethod("getchildcount");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                appendChild.call(interpreter, child2);
+                appendChild.call(interpreter, child3);
+
+                // only returns true if we are removing roSGNode type object
+                let result = removeChild.call(interpreter, child1);
+                expect(result).toEqual(BrsBoolean.True);
+                let result1 = removeChild.call(interpreter, new BrsString("some string"));
+                expect(result1).toEqual(BrsBoolean.False);
+                let result2 = removeChild.call(interpreter, new Int32(5));
+                expect(result2).toEqual(BrsBoolean.False);
+                let childCount = getChildCount.call(interpreter);
+                expect(childCount).toEqual(new Int32(2));
+            });
+        });
+
+        describe("getparent", () => {
+            it("get parent of node", () => {
+                let parent = new RoSGNode([
+                    { name: new BrsString("parent"), value: new BrsString("1") },
+                ]);
+                let child1 = new RoSGNode([
+                    { name: new BrsString("child"), value: new BrsString("2") },
+                ]);
+
+                let getParent = child1.getMethod("getparent");
+                let appendChild = parent.getMethod("appendchild");
+
+                appendChild.call(interpreter, child1);
+                let parentNode = getParent.call(interpreter);
+                expect(getParent).toBeTruthy();
+                expect(parentNode).toEqual(parent);
+            });
+
+            it("get parent of node without parent", () => {
+                let parent = new RoSGNode([
+                    { name: new BrsString("parent"), value: new BrsString("1") },
+                ]);
+                let child1 = new RoSGNode([
+                    { name: new BrsString("child"), value: new BrsString("2") },
+                ]);
+
+                let getParent = child1.getMethod("getparent");
+                let appendChild = parent.getMethod("appendchild");
+                let removeChild = parent.getMethod("removechild");
+
+                // node should start without a parent
+                let parentNode = getParent.call(interpreter);
+                expect(parentNode).toEqual(BrsInvalid.Instance);
+
+                // appends a node as child
+                appendChild.call(interpreter, child1);
+                let parentNode1 = getParent.call(interpreter);
+                expect(parentNode1).toEqual(parent);
+
+                // remove parent from child again, should get invalid as parent
+                removeChild.call(interpreter, child1);
+                let parentNode2 = getParent.call(interpreter);
+                expect(parentNode2).toEqual(BrsInvalid.Instance);
+            });
+        });
+
+        describe("createchild", () => {
+            it("create generic roSGNode as child", () => {
+                let parent = new RoSGNode([
+                    { name: new BrsString("parent"), value: new BrsString("1") },
+                ]);
+
+                let createChild = parent.getMethod("createchild");
+                let getChildren = parent.getMethod("getchildren");
+
+                let childNode = createChild.call(interpreter, new BrsString("Node"));
+                let result = getChildren.call(interpreter, new Int32(-1), new Int32(0));
+                expect(createChild).toBeTruthy();
+                expect(result.elements[0]).toEqual(childNode);
+            });
+        });
+    });
 });
diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js
index 67a3ecb..005b06a 100644
--- a/test/e2e/BrsComponents.test.js
+++ b/test/e2e/BrsComponents.test.js
@@ -91,6 +91,21 @@ describe("end to end brightscript functions", () => {
             "true",
             "can empty itself: ",
             "true",
+            //ifNodeChildren tests
+            "parent child count: ",
+            "0",
+            "get same parent from child: ",
+            "true",
+            "parent child count: ",
+            "1",
+            "parent child count: ",
+            "2",
+            "parent child count: ",
+            "3",
+            "parent child count: ",
+            "2",
+            "children size: ",
+            "2",
         ]);
     });
 
diff --git a/test/e2e/resources/components/roSGNode.brs b/test/e2e/resources/components/roSGNode.brs
index 5f335f2..6c22c9c 100644
--- a/test/e2e/resources/components/roSGNode.brs
+++ b/test/e2e/resources/components/roSGNode.brs
@@ -16,4 +16,22 @@ sub main()
 
     node1.clear()
     print "can empty itself: " node1.count() = 0                   ' => true
+
+    'ifNodeChildren tests
+    parentNode = createObject("roSGNode", "Node")
+    parentNode.id = "parent"
+    print "parent child count: " parentNode.getChildCount()        ' => 0
+    childNode = parentNode.createChild("Node")
+    testParent = childNode.getParent()
+    print "get same parent from child: " parentNode.id = testParent.id '=> true
+    print "parent child count: " parentNode.getChildCount()        ' => 1
+    childNode2 = createObject("roSGNode", "Node")
+    parentNode.appendChild(childNode2)
+    print "parent child count: " parentNode.getChildCount()        ' => 2
+    childNode3 = parentNode.createChild("Node")
+    print "parent child count: " parentNode.getChildCount()        ' => 3
+    parentNode.removeChild(childNode)
+    print "parent child count: " parentNode.getChildCount()        ' => 2
+    children = parentNode.getChildren(-1, 0)
+    print "children size: " children.count()                       ' => 2
 end sub