From 72c69217122cf5c57705c306d3161679b3442e43 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 2 Jul 2019 08:51:40 -0700 Subject: [PATCH 01/65] Implement shallow wrapper --- src/shallow.lua | 128 ++++++++++ src/shallow.spec.lua | 588 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 716 insertions(+) create mode 100644 src/shallow.lua create mode 100644 src/shallow.spec.lua diff --git a/src/shallow.lua b/src/shallow.lua new file mode 100644 index 00000000..0d412edf --- /dev/null +++ b/src/shallow.lua @@ -0,0 +1,128 @@ +local createReconciler = require(script.Parent.createReconciler) +local RobloxRenderer = require(script.Parent.RobloxRenderer) +local ElementKind = require(script.Parent.ElementKind) +local ElementUtils = require(script.Parent.ElementUtils) + +local robloxReconciler = createReconciler(RobloxRenderer) + +local function getTypeFromVirtualNode(virtualNode) + local element = virtualNode.currentElement + local kind = ElementKind.of(element) + + if kind == ElementKind.Host then + return { + kind = ElementKind.Host, + className = element.component, + } + elseif kind == ElementKind.Function then + return { + kind = ElementKind.Function, + functionComponent = element.component, + } + elseif kind == ElementKind.Stateful then + return { + kind = ElementKind.Stateful, + component = element.component, + } + else + error('>>> unkown element ' .. tostring(kind)) + end +end + +local ShallowWrapper = {} +local ShallowWrapperMetatable = { + __index = ShallowWrapper, +} + +function ShallowWrapper.new(virtualNode) + local wrapper = { + _virtualNode = virtualNode, + type = getTypeFromVirtualNode(virtualNode), + props = virtualNode.currentElement.props, + } + + return setmetatable(wrapper, ShallowWrapperMetatable) +end + +function ShallowWrapper:childrenCount() + local count = 0 + + for _ in pairs(self._virtualNode.children) do + count = count + 1 + end + + return count +end + +function ShallowWrapper:find(constraints) + local results = {} + + for _, child in pairs(self._virtualNode.children) do + if self:_satisfiesAllContraints(child, constraints) then + table.insert(results, ShallowWrapper.new(child)) + end + end + + return results +end + +function ShallowWrapper:_satisfiesAllContraints(virtualNode, constraints) + for constraint, value in pairs(constraints) do + if not self:_satisfiesConstraint(virtualNode, constraint, value) then + return false + end + end + + return true +end + +function ShallowWrapper:_satisfiesConstraint(virtualNode, constraint, value) + local element = virtualNode.currentElement + + if constraint == "kind" then + return ElementKind.of(element) == value + + elseif constraint == "className" then + local isHost = ElementKind.of(element) == ElementKind.Host + return isHost and element.component == value + + elseif constraint == "component" then + return element.component == value + + elseif constraint == "props" then + local elementProps = element.props + + for propKey, propValue in pairs(value) do + if elementProps[propKey] ~= propValue then + return false + end + end + + return true + else + error(('unknown constraint %q'):format(constraint)) + end +end + +local function shallow(element, options) + options = options or {} + + local tempParent = Instance.new("Folder") + local virtualNode = robloxReconciler.mountVirtualNode(element, tempParent, "ShallowTree") + + local maxDepth = options.depth or 1 + local currentDepth = 0 + + local nextNode = virtualNode + local wrapVirtualNode + + repeat + wrapVirtualNode = nextNode + nextNode = wrapVirtualNode.children[ElementUtils.UseParentKey] + currentDepth = currentDepth + 1 + until currentDepth > maxDepth or nextNode == nil + + return ShallowWrapper.new(wrapVirtualNode) +end + +return shallow \ No newline at end of file diff --git a/src/shallow.spec.lua b/src/shallow.spec.lua new file mode 100644 index 00000000..d83cfe12 --- /dev/null +++ b/src/shallow.spec.lua @@ -0,0 +1,588 @@ +return function() + local shallow = require(script.Parent.shallow) + + local ElementKind = require(script.Parent.ElementKind) + local createElement = require(script.Parent.createElement) + local RoactComponent = require(script.Parent.Component) + + describe("single host element", function() + local className = "TextLabel" + + local function Component(props) + return createElement(className, props) + end + + it("should have it's type.kind to Host", function() + local element = createElement(Component) + + local result = shallow(element) + + expect(result.type.kind).to.equal(ElementKind.Host) + end) + + it("should have its type.className to given instance class", function() + local element = createElement(Component) + + local result = shallow(element) + + expect(result.type.className).to.equal(className) + end) + + it("children count should be zero", function() + local element = createElement(Component) + + local result = shallow(element) + + expect(result:childrenCount()).to.equal(0) + end) + end) + + describe("single function element", function() + local function FunctionComponent(props) + return createElement("TextLabel") + end + + local function Component(props) + return createElement(FunctionComponent, props) + end + + it("should have its type.kind to Function", function() + local element = createElement(Component) + + local result = shallow(element) + + expect(result.type.kind).to.equal(ElementKind.Function) + end) + + it("should have its type.functionComponent to Function", function() + local element = createElement(Component) + + local result = shallow(element) + + expect(result.type.functionComponent).to.equal(FunctionComponent) + end) + end) + + describe("single stateful element", function() + local StatefulComponent = RoactComponent:extend("StatefulComponent") + + function StatefulComponent:render() + return createElement("TextLabel") + end + + local function Component(props) + return createElement(StatefulComponent, props) + end + + it("should have its type.kind to Stateful", function() + local element = createElement(Component) + + local result = shallow(element) + + expect(result.type.kind).to.equal(ElementKind.Stateful) + end) + + it("should have its type.component to given component class", function() + local element = createElement(Component) + + local result = shallow(element) + + expect(result.type.component).to.equal(StatefulComponent) + end) + end) + + describe("depth", function() + local unwrappedClassName = "TextLabel" + local function A(props) + return createElement(unwrappedClassName) + end + + local function B(props) + return createElement(A) + end + + local function Component(props) + return createElement(B) + end + + local function ComponentWithChildren(props) + return createElement("Frame", {}, { + ChildA = createElement(A), + ChildB = createElement(B), + }) + end + + it("should unwrap function components when depth has not exceeded", function() + local element = createElement(Component) + + local result = shallow(element, { + depth = 3, + }) + + expect(result.type.kind).to.equal(ElementKind.Host) + expect(result.type.className).to.equal(unwrappedClassName) + end) + + it("should stop unwrapping function components when depth has exceeded", function() + local element = createElement(Component) + + local result = shallow(element, { + depth = 2, + }) + + expect(result.type.kind).to.equal(ElementKind.Function) + expect(result.type.functionComponent).to.equal(A) + end) + + it("should not unwrap the element when depth is zero", function() + local element = createElement(Component) + + local result = shallow(element, { + depth = 0, + }) + + expect(result.type.kind).to.equal(ElementKind.Function) + expect(result.type.functionComponent).to.equal(Component) + end) + + it("should not unwrap children when depth is one", function() + local element = createElement(ComponentWithChildren) + + local result = shallow(element, { + depth = 1, + }) + + local childA = result:find({ + component = A, + }) + expect(#childA).to.equal(1) + + local childB = result:find({ + component = B, + }) + expect(#childB).to.equal(1) + end) + end) + + describe("childrenCount", function() + local childClassName = "TextLabel" + + local function Component(props) + local children = {} + + for i=1, props.childrenCount do + children[("Key%d"):format(i)] = createElement(childClassName) + end + + return createElement("Frame", {}, children) + end + + it("should return 1 when the element contains only one child element", function() + local element = createElement(Component, { + childrenCount = 1, + }) + + local result = shallow(element) + + expect(result:childrenCount()).to.equal(1) + end) + + it("should return 0 when the element does not contain elements", function() + local element = createElement(Component, { + childrenCount = 0, + }) + + local result = shallow(element) + + expect(result:childrenCount()).to.equal(0) + end) + end) + + describe("props", function() + it("should contains the same props using Host element", function() + local function Component(props) + return createElement("Frame", props) + end + + local props = { + BackgroundTransparency = 1, + Visible = false, + } + local element = createElement(Component, props) + + local result = shallow(element) + + expect(result.type.kind).to.equal(ElementKind.Host) + expect(result.props).to.be.ok() + + for key, value in pairs(props) do + expect(result.props[key]).to.equal(value) + end + for key, value in pairs(result.props) do + expect(props[key]).to.equal(value) + end + end) + + it("should have the same props using function element", function() + local function ChildComponent(props) + return createElement("Frame", props) + end + + local function Component(props) + return createElement(ChildComponent, props) + end + + local props = { + BackgroundTransparency = 1, + Visible = false, + } + local element = createElement(Component, props) + + local result = shallow(element) + + expect(result.type.kind).to.equal(ElementKind.Function) + expect(result.props).to.be.ok() + + for key, value in pairs(props) do + expect(result.props[key]).to.equal(value) + end + for key, value in pairs(result.props) do + expect(props[key]).to.equal(value) + end + end) + + it("should not have the children property", function() + local function ComponentWithChildren(props) + return createElement("Frame", props, { + Key = createElement("TextLabel"), + }) + end + + local props = { + BackgroundTransparency = 1, + Visible = false, + } + local element = createElement(ComponentWithChildren, props) + + local result = shallow(element) + + expect(result.props).to.be.ok() + + for key, value in pairs(props) do + expect(result.props[key]).to.equal(value) + end + for key, value in pairs(result.props) do + expect(props[key]).to.equal(value) + end + end) + + it("should have the inherited props", function() + local function Component(props) + local frameProps = { + LayoutOrder = 7, + } + for key, value in pairs(props) do + frameProps[key] = value + end + + return createElement("Frame", frameProps) + end + + local element = createElement(Component, { + BackgroundTransparency = 1, + Visible = false, + }) + + local result = shallow(element) + + expect(result.props).to.be.ok() + + local expectProps = { + BackgroundTransparency = 1, + Visible = false, + LayoutOrder = 7, + } + + for key, value in pairs(expectProps) do + expect(result.props[key]).to.equal(value) + end + for key, value in pairs(result.props) do + expect(expectProps[key]).to.equal(value) + end + end) + end) + + describe("find children", function() + local function Component(props) + return createElement("Frame", {}, props.children) + end + + describe("kind constraint", function() + it("should find the child element", function() + local childClassName = "TextLabel" + local element = createElement(Component, { + children = { + Child = createElement(childClassName), + }, + }) + + local result = shallow(element) + + local constraints = { + kind = ElementKind.Host, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Host) + expect(child.type.className).to.equal(childClassName) + end) + + it("should return an empty list when no children is found", function() + local element = createElement(Component, { + children = { + Child = createElement("TextLabel"), + }, + }) + + local result = shallow(element) + + local constraints = { + kind = ElementKind.Function, + } + local children = result:find(constraints) + + expect(next(children)).never.to.be.ok() + end) + end) + + describe("className constraint", function() + it("should find the child element", function() + local childClassName = "TextLabel" + local element = createElement(Component, { + children = { + Child = createElement(childClassName), + }, + }) + + local result = shallow(element) + + local constraints = { + className = childClassName, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Host) + expect(child.type.className).to.equal(childClassName) + end) + + it("should return an empty list when no children is found", function() + local element = createElement(Component, { + children = { + Child = createElement("TextLabel"), + }, + }) + + local result = shallow(element) + + local constraints = { + className = "Frame", + } + local children = result:find(constraints) + + expect(next(children)).never.to.be.ok() + end) + end) + + describe("component constraint", function() + it("should find the child element by it's class name", function() + local childClassName = "TextLabel" + local element = createElement(Component, { + children = { + Child = createElement(childClassName), + }, + }) + + local result = shallow(element) + + local constraints = { + component = childClassName, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Host) + expect(child.type.className).to.equal(childClassName) + end) + + it("should find the child element by it's function", function() + local function ChildComponent(props) + return nil + end + + local element = createElement(Component, { + children = { + Child = createElement(ChildComponent), + }, + }) + + local result = shallow(element) + + local constraints = { + component = ChildComponent, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Function) + expect(child.type.functionComponent).to.equal(ChildComponent) + end) + + it("should find the child element by it's component class", function() + local ChildComponent = RoactComponent:extend("ChildComponent") + + function ChildComponent:render() + return createElement("TextLabel") + end + + local element = createElement(Component, { + children = { + Child = createElement(ChildComponent), + }, + }) + + local result = shallow(element) + + local constraints = { + component = ChildComponent, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Stateful) + expect(child.type.component).to.equal(ChildComponent) + end) + + it("should return an empty list when no children is found", function() + local element = createElement(Component, { + children = { + Child = createElement("TextLabel"), + }, + }) + + local result = shallow(element) + + local constraints = { + component = "Frame", + } + local children = result:find(constraints) + + expect(next(children)).never.to.be.ok() + end) + end) + + describe("props constraint", function() + it("should find the child element that satisfies all prop constraints", function() + local childClassName = "Frame" + local props = { + Visible = false, + LayoutOrder = 7, + } + local element = createElement(Component, { + children = { + Child = createElement(childClassName, props), + }, + }) + + local result = shallow(element) + + local constraints = { + props = { + Visible = false, + LayoutOrder = 7, + }, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Host) + expect(child.type.className).to.equal(childClassName) + end) + + it("should find the child element from a subset of props", function() + local childClassName = "Frame" + local props = { + Visible = false, + LayoutOrder = 7, + } + local element = createElement(Component, { + children = { + Child = createElement(childClassName, props), + }, + }) + + local result = shallow(element) + + local constraints = { + props = { + LayoutOrder = 7, + }, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Host) + expect(child.type.className).to.equal(childClassName) + end) + + it("should return an empty list when no children is found", function() + local element = createElement(Component, { + children = { + Child = createElement("TextLabel", { + Visible = false, + LayoutOrder = 7, + }), + }, + }) + + local result = shallow(element) + + local constraints = { + props = { + Visible = false, + LayoutOrder = 4, + }, + } + local children = result:find(constraints) + + expect(next(children)).never.to.be.ok() + end) + end) + + -- it("should return children that matches all contraints", function() + + -- end) + end) +end \ No newline at end of file From 2b7dde955ca21b459466e27ed872a910f6dcb970 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 2 Jul 2019 11:08:33 -0700 Subject: [PATCH 02/65] Add depth to children wrapper --- src/shallow.lua | 107 ++++++++++++++++++++++++------------------- src/shallow.spec.lua | 91 +++++++++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 48 deletions(-) diff --git a/src/shallow.lua b/src/shallow.lua index 0d412edf..a79d7a7e 100644 --- a/src/shallow.lua +++ b/src/shallow.lua @@ -25,18 +25,61 @@ local function getTypeFromVirtualNode(virtualNode) component = element.component, } else - error('>>> unkown element ' .. tostring(kind)) + error(('shallow wrapper does not support element of kind %q'):format(kind)) end end +local function findNextVirtualNode(virtualNode, maxDepth) + local currentDepth = 0 + local wrapVirtualNode = virtualNode + local nextNode = wrapVirtualNode.children[ElementUtils.UseParentKey] + + while currentDepth < maxDepth and nextNode ~= nil do + wrapVirtualNode = nextNode + nextNode = wrapVirtualNode.children[ElementUtils.UseParentKey] + currentDepth = currentDepth + 1 + end + + return wrapVirtualNode +end + +local ContraintFunctions = { + kind = function(virtualNode, expectKind) + return ElementKind.of(virtualNode.currentElement) == expectKind + end, + className = function(virtualNode, className) + local element = virtualNode.currentElement + local isHost = ElementKind.of(element) == ElementKind.Host + return isHost and element.component == className + end, + component = function(virtualNode, expectComponentValue) + return virtualNode.currentElement.component == expectComponentValue + end, + props = function(virtualNode, propSubSet) + local elementProps = virtualNode.currentElement.props + + for propKey, propValue in pairs(propSubSet) do + if elementProps[propKey] ~= propValue then + return false + end + end + + return true + end +} + local ShallowWrapper = {} local ShallowWrapperMetatable = { __index = ShallowWrapper, } -function ShallowWrapper.new(virtualNode) +function ShallowWrapper.new(virtualNode, maxDepth) + virtualNode = findNextVirtualNode(virtualNode, maxDepth) + local wrapper = { _virtualNode = virtualNode, + _childrenMaxDepth = maxDepth - 1, + _children = maxDepth == 0 and {} or virtualNode.children, type = getTypeFromVirtualNode(virtualNode), props = virtualNode.currentElement.props, } @@ -47,7 +90,7 @@ end function ShallowWrapper:childrenCount() local count = 0 - for _ in pairs(self._virtualNode.children) do + for _ in pairs(self._children) do count = count + 1 end @@ -57,9 +100,17 @@ end function ShallowWrapper:find(constraints) local results = {} - for _, child in pairs(self._virtualNode.children) do - if self:_satisfiesAllContraints(child, constraints) then - table.insert(results, ShallowWrapper.new(child)) + for constraint in pairs(constraints) do + if not ContraintFunctions[constraint] then + error(('unknown constraint %q'):format(constraint)) + end + end + + for _, child in pairs(self._children) do + local childWrapper = ShallowWrapper.new(child, self._childrenMaxDepth) + + if self:_satisfiesAllContraints(childWrapper._virtualNode, constraints) then + table.insert(results, childWrapper) end end @@ -68,7 +119,9 @@ end function ShallowWrapper:_satisfiesAllContraints(virtualNode, constraints) for constraint, value in pairs(constraints) do - if not self:_satisfiesConstraint(virtualNode, constraint, value) then + local constraintFunction = ContraintFunctions[constraint] + + if not constraintFunction(virtualNode, value) then return false end end @@ -76,34 +129,6 @@ function ShallowWrapper:_satisfiesAllContraints(virtualNode, constraints) return true end -function ShallowWrapper:_satisfiesConstraint(virtualNode, constraint, value) - local element = virtualNode.currentElement - - if constraint == "kind" then - return ElementKind.of(element) == value - - elseif constraint == "className" then - local isHost = ElementKind.of(element) == ElementKind.Host - return isHost and element.component == value - - elseif constraint == "component" then - return element.component == value - - elseif constraint == "props" then - local elementProps = element.props - - for propKey, propValue in pairs(value) do - if elementProps[propKey] ~= propValue then - return false - end - end - - return true - else - error(('unknown constraint %q'):format(constraint)) - end -end - local function shallow(element, options) options = options or {} @@ -111,18 +136,8 @@ local function shallow(element, options) local virtualNode = robloxReconciler.mountVirtualNode(element, tempParent, "ShallowTree") local maxDepth = options.depth or 1 - local currentDepth = 0 - - local nextNode = virtualNode - local wrapVirtualNode - - repeat - wrapVirtualNode = nextNode - nextNode = wrapVirtualNode.children[ElementUtils.UseParentKey] - currentDepth = currentDepth + 1 - until currentDepth > maxDepth or nextNode == nil - return ShallowWrapper.new(wrapVirtualNode) + return ShallowWrapper.new(virtualNode, maxDepth) end return shallow \ No newline at end of file diff --git a/src/shallow.spec.lua b/src/shallow.spec.lua index d83cfe12..8d25d8de 100644 --- a/src/shallow.spec.lua +++ b/src/shallow.spec.lua @@ -162,6 +162,57 @@ return function() }) expect(#childB).to.equal(1) end) + + it("should unwrap children when depth is two", function() + local element = createElement(ComponentWithChildren) + + local result = shallow(element, { + depth = 2, + }) + + local hostChild = result:find({ + component = unwrappedClassName, + }) + expect(#hostChild).to.equal(1) + + local unwrappedBChild = result:find({ + component = A, + }) + expect(#unwrappedBChild).to.equal(1) + end) + + it("should not include any children when depth is zero", function() + local element = createElement(ComponentWithChildren) + + local result = shallow(element, { + depth = 0, + }) + + expect(result:childrenCount()).to.equal(0) + end) + + it("should not include any grand-children when depth is one", function() + local function ParentComponent() + return createElement("Folder", {}, { + Child = createElement(ComponentWithChildren), + }) + end + + local element = createElement(ParentComponent) + + local result = shallow(element, { + depth = 1, + }) + + expect(result:childrenCount()).to.equal(1) + + local componentWithChildrenWrapper = result:find({ + component = ComponentWithChildren, + })[1] + expect(componentWithChildrenWrapper).to.be.ok() + + expect(componentWithChildrenWrapper:childrenCount()).to.equal(0) + end) end) describe("childrenCount", function() @@ -581,8 +632,44 @@ return function() end) end) - -- it("should return children that matches all contraints", function() + it("should throw if the constraint does not exist", function() + local element = createElement("Frame") + + local result = shallow(element) + + local function findWithInvalidConstraint() + result:find({ + nothing = false, + }) + end + + expect(findWithInvalidConstraint).to.throw() + end) - -- end) + it("should return children that matches all contraints", function() + local function ComponentWithChildren() + return createElement("Frame", {}, { + ChildA = createElement("TextLabel", { + Visible = false, + }), + ChildB = createElement("TextButton", { + Visible = false, + }), + }) + end + + local element = createElement(ComponentWithChildren) + + local result = shallow(element) + + local children = result:find({ + className = "TextLabel", + props = { + Visible = false, + }, + }) + + expect(#children).to.equal(1) + end) end) end \ No newline at end of file From f57fa89eeb6932b2ed5066ec111a5ff1e5484736 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 2 Jul 2019 15:36:12 -0700 Subject: [PATCH 03/65] Support fragments --- src/shallow.lua | 77 ++++++++++++++++++++++++++++++-------------- src/shallow.spec.lua | 76 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 24 deletions(-) diff --git a/src/shallow.lua b/src/shallow.lua index a79d7a7e..b20d8e33 100644 --- a/src/shallow.lua +++ b/src/shallow.lua @@ -5,6 +5,11 @@ local ElementUtils = require(script.Parent.ElementUtils) local robloxReconciler = createReconciler(RobloxRenderer) +local ShallowWrapper = {} +local ShallowWrapperMetatable = { + __index = ShallowWrapper, +} + local function getTypeFromVirtualNode(virtualNode) local element = virtualNode.currentElement local kind = ElementKind.of(element) @@ -25,7 +30,7 @@ local function getTypeFromVirtualNode(virtualNode) component = element.component, } else - error(('shallow wrapper does not support element of kind %q'):format(kind)) + error(('shallow wrapper does not support element of kind %q'):format(tostring(kind))) end end @@ -44,19 +49,18 @@ local function findNextVirtualNode(virtualNode, maxDepth) end local ContraintFunctions = { - kind = function(virtualNode, expectKind) - return ElementKind.of(virtualNode.currentElement) == expectKind + kind = function(element, expectKind) + return ElementKind.of(element) == expectKind end, - className = function(virtualNode, className) - local element = virtualNode.currentElement + className = function(element, className) local isHost = ElementKind.of(element) == ElementKind.Host return isHost and element.component == className end, - component = function(virtualNode, expectComponentValue) - return virtualNode.currentElement.component == expectComponentValue + component = function(element, expectComponentValue) + return element.component == expectComponentValue end, - props = function(virtualNode, propSubSet) - local elementProps = virtualNode.currentElement.props + props = function(element, propSubSet) + local elementProps = element.props for propKey, propValue in pairs(propSubSet) do if elementProps[propKey] ~= propValue then @@ -68,10 +72,36 @@ local ContraintFunctions = { end } -local ShallowWrapper = {} -local ShallowWrapperMetatable = { - __index = ShallowWrapper, -} +local function countChildrenOfElement(element) + if ElementKind.of(element) == ElementKind.Fragment then + local count = 0 + + for _, subElement in pairs(element.elements) do + count = count + countChildrenOfElement(subElement) + end + + return count + else + return 1 + end +end + +local function findChildren(virtualNode, constraints, results, maxDepth) + if ElementKind.of(virtualNode.currentElement) == ElementKind.Fragment then + for _, subVirtualNode in pairs(virtualNode.children) do + findChildren(subVirtualNode, constraints, results, maxDepth) + end + else + local childWrapper = ShallowWrapper.new( + virtualNode, + maxDepth + ) + + if childWrapper:_satisfiesAllContraints(constraints) then + table.insert(results, childWrapper) + end + end +end function ShallowWrapper.new(virtualNode, maxDepth) virtualNode = findNextVirtualNode(virtualNode, maxDepth) @@ -90,38 +120,37 @@ end function ShallowWrapper:childrenCount() local count = 0 - for _ in pairs(self._children) do - count = count + 1 + for _, virtualNode in pairs(self._children) do + local element = virtualNode.currentElement + count = count + countChildrenOfElement(element) end return count end function ShallowWrapper:find(constraints) - local results = {} - for constraint in pairs(constraints) do if not ContraintFunctions[constraint] then error(('unknown constraint %q'):format(constraint)) end end - for _, child in pairs(self._children) do - local childWrapper = ShallowWrapper.new(child, self._childrenMaxDepth) + local results = {} - if self:_satisfiesAllContraints(childWrapper._virtualNode, constraints) then - table.insert(results, childWrapper) - end + for _, childVirtualNode in pairs(self._children) do + findChildren(childVirtualNode, constraints, results, self._childrenMaxDepth) end return results end -function ShallowWrapper:_satisfiesAllContraints(virtualNode, constraints) +function ShallowWrapper:_satisfiesAllContraints(constraints) + local element = self._virtualNode.currentElement + for constraint, value in pairs(constraints) do local constraintFunction = ContraintFunctions[constraint] - if not constraintFunction(virtualNode, value) then + if not constraintFunction(element, value) then return false end end diff --git a/src/shallow.spec.lua b/src/shallow.spec.lua index 8d25d8de..585a89a7 100644 --- a/src/shallow.spec.lua +++ b/src/shallow.spec.lua @@ -3,6 +3,7 @@ return function() local ElementKind = require(script.Parent.ElementKind) local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) local RoactComponent = require(script.Parent.Component) describe("single host element", function() @@ -247,6 +248,35 @@ return function() expect(result:childrenCount()).to.equal(0) end) + + it("should count children in a fragment", function() + local element = createElement("Frame", {}, { + Frag = createFragment({ + Label = createElement("TextLabel"), + Button = createElement("TextButton"), + }) + }) + + local result = shallow(element) + + expect(result:childrenCount()).to.equal(2) + end) + + it("should count children nested in fragments", function() + local element = createElement("Frame", {}, { + Frag = createFragment({ + SubFrag = createFragment({ + Frame = createElement("Frame"), + }), + Label = createElement("TextLabel"), + Button = createElement("TextButton"), + }) + }) + + local result = shallow(element) + + expect(result:childrenCount()).to.equal(3) + end) end) describe("props", function() @@ -671,5 +701,51 @@ return function() expect(#children).to.equal(1) end) + + it("should return children from fragments", function() + local childClassName = "TextLabel" + + local function ComponentWithFragment() + return createElement("Frame", {}, { + Fragment = createFragment({ + Child = createElement(childClassName), + }), + }) + end + + local element = createElement(ComponentWithFragment) + + local result = shallow(element) + + local children = result:find({ + className = childClassName + }) + + expect(#children).to.equal(1) + end) + + it("should return children from nested fragments", function() + local childClassName = "TextLabel" + + local function ComponentWithFragment() + return createElement("Frame", {}, { + Fragment = createFragment({ + SubFragment = createFragment({ + Child = createElement(childClassName), + }), + }), + }) + end + + local element = createElement(ComponentWithFragment) + + local result = shallow(element) + + local children = result:find({ + className = childClassName + }) + + expect(#children).to.equal(1) + end) end) end \ No newline at end of file From 7ee29392ac81b9db0da6b980439b302e4ff61cea Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 9 Jul 2019 10:50:50 -0700 Subject: [PATCH 04/65] Filter children prop from shallow.props --- src/shallow.lua | 19 ++++++++++++++++++- src/shallow.spec.lua | 36 +++++++++++------------------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/shallow.lua b/src/shallow.lua index b20d8e33..1073fdb8 100644 --- a/src/shallow.lua +++ b/src/shallow.lua @@ -1,4 +1,5 @@ local createReconciler = require(script.Parent.createReconciler) +local Children = require(script.Parent.PropMarkers.Children) local RobloxRenderer = require(script.Parent.RobloxRenderer) local ElementKind = require(script.Parent.ElementKind) local ElementUtils = require(script.Parent.ElementUtils) @@ -103,6 +104,22 @@ local function findChildren(virtualNode, constraints, results, maxDepth) end end +local function filterProps(props) + if props[Children] == nil then + return props + end + + local filteredProps = {} + + for key, value in pairs(props) do + if key ~= Children then + props[key] = value + end + end + + return filteredProps +end + function ShallowWrapper.new(virtualNode, maxDepth) virtualNode = findNextVirtualNode(virtualNode, maxDepth) @@ -111,7 +128,7 @@ function ShallowWrapper.new(virtualNode, maxDepth) _childrenMaxDepth = maxDepth - 1, _children = maxDepth == 0 and {} or virtualNode.children, type = getTypeFromVirtualNode(virtualNode), - props = virtualNode.currentElement.props, + props = filterProps(virtualNode.currentElement.props), } return setmetatable(wrapper, ShallowWrapperMetatable) diff --git a/src/shallow.spec.lua b/src/shallow.spec.lua index 585a89a7..66ec0212 100644 --- a/src/shallow.spec.lua +++ b/src/shallow.spec.lua @@ -1,6 +1,8 @@ return function() local shallow = require(script.Parent.shallow) + local assertDeepEqual = require(script.Parent.assertDeepEqual) + local Children = require(script.Parent.PropMarkers.Children) local ElementKind = require(script.Parent.ElementKind) local createElement = require(script.Parent.createElement) local createFragment = require(script.Parent.createFragment) @@ -296,12 +298,7 @@ return function() expect(result.type.kind).to.equal(ElementKind.Host) expect(result.props).to.be.ok() - for key, value in pairs(props) do - expect(result.props[key]).to.equal(value) - end - for key, value in pairs(result.props) do - expect(props[key]).to.equal(value) - end + assertDeepEqual(props, result.props) end) it("should have the same props using function element", function() @@ -317,6 +314,10 @@ return function() BackgroundTransparency = 1, Visible = false, } + local propsCopy = {} + for key, value in pairs(props) do + propsCopy[key] = value + end local element = createElement(Component, props) local result = shallow(element) @@ -324,12 +325,7 @@ return function() expect(result.type.kind).to.equal(ElementKind.Function) expect(result.props).to.be.ok() - for key, value in pairs(props) do - expect(result.props[key]).to.equal(value) - end - for key, value in pairs(result.props) do - expect(props[key]).to.equal(value) - end + assertDeepEqual(propsCopy, result.props) end) it("should not have the children property", function() @@ -343,18 +339,13 @@ return function() BackgroundTransparency = 1, Visible = false, } + local element = createElement(ComponentWithChildren, props) local result = shallow(element) expect(result.props).to.be.ok() - - for key, value in pairs(props) do - expect(result.props[key]).to.equal(value) - end - for key, value in pairs(result.props) do - expect(props[key]).to.equal(value) - end + expect(result.props[Children]).never.to.be.ok() end) it("should have the inherited props", function() @@ -384,12 +375,7 @@ return function() LayoutOrder = 7, } - for key, value in pairs(expectProps) do - expect(result.props[key]).to.equal(value) - end - for key, value in pairs(result.props) do - expect(expectProps[key]).to.equal(value) - end + assertDeepEqual(expectProps, result.props) end) end) From 45b48dae046599ca3f32b57c94b0d2ab84efdb2b Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 9 Jul 2019 14:31:10 -0700 Subject: [PATCH 05/65] Add ShallowWrapper:getChildren() --- src/shallow.lua | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/shallow.lua b/src/shallow.lua index 1073fdb8..df76a868 100644 --- a/src/shallow.lua +++ b/src/shallow.lua @@ -87,10 +87,10 @@ local function countChildrenOfElement(element) end end -local function findChildren(virtualNode, constraints, results, maxDepth) +local function getChildren(virtualNode, results, maxDepth) if ElementKind.of(virtualNode.currentElement) == ElementKind.Fragment then for _, subVirtualNode in pairs(virtualNode.children) do - findChildren(subVirtualNode, constraints, results, maxDepth) + getChildren(subVirtualNode, results, maxDepth) end else local childWrapper = ShallowWrapper.new( @@ -98,9 +98,7 @@ local function findChildren(virtualNode, constraints, results, maxDepth) maxDepth ) - if childWrapper:_satisfiesAllContraints(constraints) then - table.insert(results, childWrapper) - end + table.insert(results, childWrapper) end end @@ -126,7 +124,8 @@ function ShallowWrapper.new(virtualNode, maxDepth) local wrapper = { _virtualNode = virtualNode, _childrenMaxDepth = maxDepth - 1, - _children = maxDepth == 0 and {} or virtualNode.children, + _virtualNodeChildren = maxDepth == 0 and {} or virtualNode.children, + _shallowChildren = nil, type = getTypeFromVirtualNode(virtualNode), props = filterProps(virtualNode.currentElement.props), } @@ -137,7 +136,7 @@ end function ShallowWrapper:childrenCount() local count = 0 - for _, virtualNode in pairs(self._children) do + for _, virtualNode in pairs(self._virtualNodeChildren) do local element = virtualNode.currentElement count = count + countChildrenOfElement(element) end @@ -152,12 +151,31 @@ function ShallowWrapper:find(constraints) end end + local results = {} + local children = self:getChildren() + + for i=1, #children do + local childWrapper = children[i] + if childWrapper:_satisfiesAllContraints(constraints) then + table.insert(results, childWrapper) + end + end + + return results +end + +function ShallowWrapper:getChildren() + if self._shallowChildren then + return self._shallowChildren + end + local results = {} - for _, childVirtualNode in pairs(self._children) do - findChildren(childVirtualNode, constraints, results, self._childrenMaxDepth) + for _, childVirtualNode in pairs(self._virtualNodeChildren) do + getChildren(childVirtualNode, results, self._childrenMaxDepth) end + self._shallowChildren = results return results end From 8450335ae36614121f782f16a1524ca7bdd5cf63 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 19 Jul 2019 11:53:44 -0700 Subject: [PATCH 06/65] Split deep equal comparison from assertDeepEqual --- src/assertDeepEqual.lua | 51 +------------------ src/assertDeepEqual.spec.lua | 89 +++----------------------------- src/deepEqual.lua | 51 +++++++++++++++++++ src/deepEqual.spec.lua | 99 ++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 131 deletions(-) create mode 100644 src/deepEqual.lua create mode 100644 src/deepEqual.spec.lua diff --git a/src/assertDeepEqual.lua b/src/assertDeepEqual.lua index 3f422d85..2fbda552 100644 --- a/src/assertDeepEqual.lua +++ b/src/assertDeepEqual.lua @@ -5,56 +5,7 @@ This should only be used in tests. ]] - -local function deepEqual(a, b) - if typeof(a) ~= typeof(b) then - local message = ("{1} is of type %s, but {2} is of type %s"):format( - typeof(a), - typeof(b) - ) - return false, message - end - - if typeof(a) == "table" then - local visitedKeys = {} - - for key, value in pairs(a) do - visitedKeys[key] = true - - local success, innerMessage = deepEqual(value, b[key]) - if not success then - local message = innerMessage - :gsub("{1}", ("{1}[%s]"):format(tostring(key))) - :gsub("{2}", ("{2}[%s]"):format(tostring(key))) - - return false, message - end - end - - for key, value in pairs(b) do - if not visitedKeys[key] then - local success, innerMessage = deepEqual(value, a[key]) - - if not success then - local message = innerMessage - :gsub("{1}", ("{1}[%s]"):format(tostring(key))) - :gsub("{2}", ("{2}[%s]"):format(tostring(key))) - - return false, message - end - end - end - - return true - end - - if a == b then - return true - end - - local message = "{1} ~= {2}" - return false, message -end +local deepEqual = require(script.Parent.deepEqual) local function assertDeepEqual(a, b) local success, innerMessageTemplate = deepEqual(a, b) diff --git a/src/assertDeepEqual.spec.lua b/src/assertDeepEqual.spec.lua index 484eeffd..b7610137 100644 --- a/src/assertDeepEqual.spec.lua +++ b/src/assertDeepEqual.spec.lua @@ -1,7 +1,12 @@ return function() local assertDeepEqual = require(script.Parent.assertDeepEqual) - it("should fail with a message when args are not equal", function() + it("should not throw if the args are equal", function() + assertDeepEqual(1, 1) + assertDeepEqual("hello", "hello") + end) + + it("should throw and format the error message when args are not equal", function() local success, message = pcall(assertDeepEqual, 1, 2) expect(success).to.equal(false) @@ -15,85 +20,7 @@ return function() expect(success).to.equal(false) expect(message:find("first%[foo%] ~= second%[foo%]")).to.be.ok() - end) - - it("should compare non-table values using standard '==' equality", function() - assertDeepEqual(1, 1) - assertDeepEqual("hello", "hello") - assertDeepEqual(nil, nil) - - local someFunction = function() end - local theSameFunction = someFunction - - assertDeepEqual(someFunction, theSameFunction) - - local A = { - foo = someFunction - } - local B = { - foo = theSameFunction - } - - assertDeepEqual(A, B) - end) - - it("should fail when types differ", function() - local success, message = pcall(assertDeepEqual, 1, "1") - - expect(success).to.equal(false) - expect(message:find("first is of type number, but second is of type string")).to.be.ok() - end) - - it("should compare (and report about) nested tables", function() - local A = { - foo = "bar", - nested = { - foo = 1, - bar = 2, - } - } - local B = { - foo = "bar", - nested = { - foo = 1, - bar = 2, - } - } - - assertDeepEqual(A, B) - - local C = { - foo = "bar", - nested = { - foo = 1, - bar = 3, - } - } - - local success, message = pcall(assertDeepEqual, A, C) - - expect(success).to.equal(false) - expect(message:find("first%[nested%]%[bar%] ~= second%[nested%]%[bar%]")).to.be.ok() - end) - - it("should be commutative", function() - local equalArgsA = { - foo = "bar", - hello = "world", - } - local equalArgsB = { - foo = "bar", - hello = "world", - } - - assertDeepEqual(equalArgsA, equalArgsB) - assertDeepEqual(equalArgsB, equalArgsA) - - local nonEqualArgs = { - foo = "bar", - } - - expect(function() assertDeepEqual(equalArgsA, nonEqualArgs) end).to.throw() - expect(function() assertDeepEqual(nonEqualArgs, equalArgsA) end).to.throw() + expect(message:find("{1}")).never.to.be.ok() + expect(message:find("{2}")).never.to.be.ok() end) end \ No newline at end of file diff --git a/src/deepEqual.lua b/src/deepEqual.lua new file mode 100644 index 00000000..0e052ba1 --- /dev/null +++ b/src/deepEqual.lua @@ -0,0 +1,51 @@ +local function deepEqual(a, b) + if typeof(a) ~= typeof(b) then + local message = ("{1} is of type %s, but {2} is of type %s"):format( + typeof(a), + typeof(b) + ) + return false, message + end + + if typeof(a) == "table" then + local visitedKeys = {} + + for key, value in pairs(a) do + visitedKeys[key] = true + + local success, innerMessage = deepEqual(value, b[key]) + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, message + end + end + + for key, value in pairs(b) do + if not visitedKeys[key] then + local success, innerMessage = deepEqual(value, a[key]) + + if not success then + local message = innerMessage + :gsub("{1}", ("{1}[%s]"):format(tostring(key))) + :gsub("{2}", ("{2}[%s]"):format(tostring(key))) + + return false, message + end + end + end + + return true + end + + if a == b then + return true + end + + local message = "{1} ~= {2}" + return false, message +end + +return deepEqual \ No newline at end of file diff --git a/src/deepEqual.spec.lua b/src/deepEqual.spec.lua new file mode 100644 index 00000000..41931156 --- /dev/null +++ b/src/deepEqual.spec.lua @@ -0,0 +1,99 @@ +return function() + local deepEqual = require(script.Parent.deepEqual) + + it("should compare non-table values using standard '==' equality", function() + expect(deepEqual(1, 1)).to.equal(true) + expect(deepEqual("hello", "hello")).to.equal(true) + expect(deepEqual(nil, nil)).to.equal(true) + + local someFunction = function() end + local theSameFunction = someFunction + + expect(deepEqual(someFunction, theSameFunction)).to.equal(true) + + local A = { + foo = someFunction + } + local B = { + foo = theSameFunction + } + + expect(deepEqual(A, B)).to.equal(true) + end) + + it("should fail with a message when args are not equal", function() + local success, message = deepEqual(1, 2) + + expect(success).to.equal(false) + expect(message:find("{1} ~= {2}")).to.be.ok() + + success, message = deepEqual({ + foo = 1, + }, { + foo = 2, + }) + + expect(success).to.equal(false) + expect(message:find("{1}%[foo%] ~= {2}%[foo%]")).to.be.ok() + end) + + it("should fail when types differ", function() + local success, message = deepEqual(1, "1") + + expect(success).to.equal(false) + expect(message:find("{1} is of type number, but {2} is of type string")).to.be.ok() + end) + + it("should compare (and report about) nested tables", function() + local A = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + local B = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + + deepEqual(A, B) + + local C = { + foo = "bar", + nested = { + foo = 1, + bar = 3, + } + } + + local success, message = deepEqual(A, C) + + expect(success).to.equal(false) + expect(message:find("{1}%[nested%]%[bar%] ~= {2}%[nested%]%[bar%]")).to.be.ok() + end) + + it("should be commutative", function() + local equalArgsA = { + foo = "bar", + hello = "world", + } + local equalArgsB = { + foo = "bar", + hello = "world", + } + + expect(deepEqual(equalArgsA, equalArgsB)).to.equal(true) + expect(deepEqual(equalArgsB, equalArgsA)).to.equal(true) + + local nonEqualArgs = { + foo = "bar", + } + + expect(deepEqual(equalArgsA, nonEqualArgs)).to.equal(false) + expect(deepEqual(nonEqualArgs, equalArgsA)).to.equal(false) + end) +end \ No newline at end of file From a1f914f0dff0d618e73de24b93e29528c7aa8a42 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 19 Jul 2019 13:30:03 -0700 Subject: [PATCH 07/65] Add snapshot serialization --- src/init.lua | 3 + src/init.spec.lua | 1 + src/shallow.lua | 60 +++- src/shallow.spec.lua | 131 +++++++++ src/snapshot/Serialize/AnonymousFunction.lua | 5 + src/snapshot/Serialize/IndentedOutput.lua | 54 ++++ .../Serialize/IndentedOutput.spec.lua | 72 +++++ src/snapshot/Serialize/Serializer.lua | 189 ++++++++++++ src/snapshot/Serialize/Serializer.spec.lua | 274 ++++++++++++++++++ src/snapshot/Serialize/SnapshotData.lua | 90 ++++++ src/snapshot/Serialize/SnapshotData.spec.lua | 208 +++++++++++++ src/snapshot/Serialize/init.lua | 11 + src/snapshot/Snapshot.lua | 97 +++++++ src/snapshot/Snapshot.spec.lua | 132 +++++++++ src/snapshot/init.lua | 9 + 15 files changed, 1324 insertions(+), 12 deletions(-) create mode 100644 src/snapshot/Serialize/AnonymousFunction.lua create mode 100644 src/snapshot/Serialize/IndentedOutput.lua create mode 100644 src/snapshot/Serialize/IndentedOutput.spec.lua create mode 100644 src/snapshot/Serialize/Serializer.lua create mode 100644 src/snapshot/Serialize/Serializer.spec.lua create mode 100644 src/snapshot/Serialize/SnapshotData.lua create mode 100644 src/snapshot/Serialize/SnapshotData.spec.lua create mode 100644 src/snapshot/Serialize/init.lua create mode 100644 src/snapshot/Snapshot.lua create mode 100644 src/snapshot/Snapshot.spec.lua create mode 100644 src/snapshot/init.lua diff --git a/src/init.lua b/src/init.lua index f002f975..2e33cc5d 100644 --- a/src/init.lua +++ b/src/init.lua @@ -6,6 +6,7 @@ local GlobalConfig = require(script.GlobalConfig) local createReconciler = require(script.createReconciler) local createReconcilerCompat = require(script.createReconcilerCompat) local RobloxRenderer = require(script.RobloxRenderer) +local shallow = require(script.shallow) local strict = require(script.strict) local Binding = require(script.Binding) @@ -39,6 +40,8 @@ local Roact = strict { setGlobalConfig = GlobalConfig.set, + shallow = shallow, + -- APIs that may change in the future without warning UNSTABLE = { }, diff --git a/src/init.spec.lua b/src/init.spec.lua index 7fcf79c8..390d3846 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -13,6 +13,7 @@ return function() update = "function", oneChild = "function", setGlobalConfig = "function", + shallow = "function", -- These functions are deprecated and throw warnings! reify = "function", diff --git a/src/shallow.lua b/src/shallow.lua index df76a868..01ef95d4 100644 --- a/src/shallow.lua +++ b/src/shallow.lua @@ -3,6 +3,7 @@ local Children = require(script.Parent.PropMarkers.Children) local RobloxRenderer = require(script.Parent.RobloxRenderer) local ElementKind = require(script.Parent.ElementKind) local ElementUtils = require(script.Parent.ElementUtils) +local snapshot = require(script.Parent.snapshot) local robloxReconciler = createReconciler(RobloxRenderer) @@ -50,18 +51,19 @@ local function findNextVirtualNode(virtualNode, maxDepth) end local ContraintFunctions = { - kind = function(element, expectKind) - return ElementKind.of(element) == expectKind + kind = function(virtualNode, expectKind) + return ElementKind.of(virtualNode.currentElement) == expectKind end, - className = function(element, className) + className = function(virtualNode, className) + local element = virtualNode.currentElement local isHost = ElementKind.of(element) == ElementKind.Host return isHost and element.component == className end, - component = function(element, expectComponentValue) - return element.component == expectComponentValue + component = function(virtualNode, expectComponentValue) + return virtualNode.currentElement.component == expectComponentValue end, - props = function(element, propSubSet) - local elementProps = element.props + props = function(virtualNode, propSubSet) + local elementProps = virtualNode.currentElement.props for propKey, propValue in pairs(propSubSet) do if elementProps[propKey] ~= propValue then @@ -70,7 +72,10 @@ local ContraintFunctions = { end return true - end + end, + hostKey = function(virtualNode, expectHostKey) + return virtualNode.hostKey == expectHostKey + end, } local function countChildrenOfElement(element) @@ -111,7 +116,7 @@ local function filterProps(props) for key, value in pairs(props) do if key ~= Children then - props[key] = value + filteredProps[key] = value end end @@ -128,6 +133,8 @@ function ShallowWrapper.new(virtualNode, maxDepth) _shallowChildren = nil, type = getTypeFromVirtualNode(virtualNode), props = filterProps(virtualNode.currentElement.props), + hostKey = virtualNode.hostKey, + instance = virtualNode.hostObject, } return setmetatable(wrapper, ShallowWrapperMetatable) @@ -145,7 +152,7 @@ function ShallowWrapper:childrenCount() end function ShallowWrapper:find(constraints) - for constraint in pairs(constraints) do + for constraint in pairs(constraints) do if not ContraintFunctions[constraint] then error(('unknown constraint %q'):format(constraint)) end @@ -164,6 +171,27 @@ function ShallowWrapper:find(constraints) return results end +function ShallowWrapper:findUnique(constraints) + local children = self:getChildren() + + if constraints == nil then + assert( + #children == 1, + ("expect to contain exactly one child, but found %d"):format(#children) + ) + return children[1] + end + + local constrainedChildren = self:find(constraints) + + assert( + #constrainedChildren == 1, + ("expect to find only one child, but found %d"):format(#constrainedChildren) + ) + + return constrainedChildren[1] +end + function ShallowWrapper:getChildren() if self._shallowChildren then return self._shallowChildren @@ -179,13 +207,21 @@ function ShallowWrapper:getChildren() return results end +function ShallowWrapper:toMatchSnapshot(identifier) + assert(typeof(identifier) == "string", "Snapshot identifier must be a string") + + local snapshotResult = snapshot(identifier, self) + + snapshotResult:match() +end + function ShallowWrapper:_satisfiesAllContraints(constraints) - local element = self._virtualNode.currentElement + local virtualNode = self._virtualNode for constraint, value in pairs(constraints) do local constraintFunction = ContraintFunctions[constraint] - if not constraintFunction(element, value) then + if not constraintFunction(virtualNode, value) then return false end end diff --git a/src/shallow.spec.lua b/src/shallow.spec.lua index 66ec0212..256ed98a 100644 --- a/src/shallow.spec.lua +++ b/src/shallow.spec.lua @@ -379,6 +379,37 @@ return function() end) end) + describe("instance", function() + it("should contain the instance when it is a host component", function() + local className = "Frame" + local function Component(props) + return createElement(className) + end + + local element = createElement(Component) + + local result = shallow(element) + + expect(result.instance).to.be.ok() + expect(result.instance.ClassName).to.equal(className) + end) + + it("should not have an instance if it is a function component", function() + local function Child() + return createElement("Frame") + end + local function Component(props) + return createElement(Child) + end + + local element = createElement(Component) + + local result = shallow(element) + + expect(result.instance).never.to.be.ok() + end) + end) + describe("find children", function() local function Component(props) return createElement("Frame", {}, props.children) @@ -648,6 +679,47 @@ return function() end) end) + describe("hostKey constraint", function() + it("should find the child element", function() + local hostKey = "Child" + local element = createElement(Component, { + children = { + [hostKey] = createElement("TextLabel"), + }, + }) + + local result = shallow(element) + + local constraints = { + hostKey = hostKey, + } + local children = result:find(constraints) + + expect(#children).to.equal(1) + + local child = children[1] + + expect(child.type.kind).to.equal(ElementKind.Host) + end) + + it("should return an empty list when no children is found", function() + local element = createElement(Component, { + children = { + Child = createElement("TextLabel"), + }, + }) + + local result = shallow(element) + + local constraints = { + hostKey = "NotFound", + } + local children = result:find(constraints) + + expect(next(children)).never.to.be.ok() + end) + end) + it("should throw if the constraint does not exist", function() local element = createElement("Frame") @@ -734,4 +806,63 @@ return function() expect(#children).to.equal(1) end) end) + + describe("findUnique", function() + it("should return the only child when no constraints are given", function() + local element = createElement("Frame", {}, { + Child = createElement("TextLabel"), + }) + + local result = shallow(element) + + local child = result:findUnique() + + expect(child.type.kind).to.equal(ElementKind.Host) + expect(child.type.className).to.equal("TextLabel") + end) + + it("should return the only child that satifies the constraint", function() + local element = createElement("Frame", {}, { + ChildA = createElement("TextLabel"), + ChildB = createElement("TextButton"), + }) + + local result = shallow(element) + + local child = result:findUnique({ + className = "TextLabel", + }) + + expect(child.type.className).to.equal("TextLabel") + end) + + it("should throw if there is not any child element", function() + local element = createElement("Frame") + + local result = shallow(element) + + local function shouldThrow() + result:findUnique() + end + + expect(shouldThrow).to.throw() + end) + + it("should throw if more than one child satisfies the constraint", function() + local element = createElement("Frame", {}, { + ChildA = createElement("TextLabel"), + ChildB = createElement("TextLabel"), + }) + + local result = shallow(element) + + local function shouldThrow() + result:findUnique({ + className = "TextLabel", + }) + end + + expect(shouldThrow).to.throw() + end) + end) end \ No newline at end of file diff --git a/src/snapshot/Serialize/AnonymousFunction.lua b/src/snapshot/Serialize/AnonymousFunction.lua new file mode 100644 index 00000000..62be7341 --- /dev/null +++ b/src/snapshot/Serialize/AnonymousFunction.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Parent.Symbol) + +local AnonymousFunction = Symbol.named("AnonymousFunction") + +return AnonymousFunction \ No newline at end of file diff --git a/src/snapshot/Serialize/IndentedOutput.lua b/src/snapshot/Serialize/IndentedOutput.lua new file mode 100644 index 00000000..cfef8376 --- /dev/null +++ b/src/snapshot/Serialize/IndentedOutput.lua @@ -0,0 +1,54 @@ +local IndentedOutput = {} +local IndentedOutputMetatable = { + __index = IndentedOutput, +} + +function IndentedOutput.new(indentation) + indentation = indentation or 2 + + local output = { + _level = 0, + _indentation = (" "):rep(indentation), + _lines = {}, + } + + setmetatable(output, IndentedOutputMetatable) + + return output +end + +function IndentedOutput:write(line, ...) + if select("#", ...) > 0 then + line = line:format(...) + end + + local indentedLine = ("%s%s"):format(self._indentation:rep(self._level), line) + + table.insert(self._lines, indentedLine) +end + +function IndentedOutput:push() + self._level = self._level + 1 +end + +function IndentedOutput:pop() + self._level = math.max(self._level - 1, 0) +end + +function IndentedOutput:writeAndPush(line) + self:write(line) + self:push() +end + +function IndentedOutput:popAndWrite(line) + self:pop() + self:write(line) +end + +function IndentedOutput:join(separator) + separator = separator or "\n" + + return table.concat(self._lines, separator) +end + +return IndentedOutput diff --git a/src/snapshot/Serialize/IndentedOutput.spec.lua b/src/snapshot/Serialize/IndentedOutput.spec.lua new file mode 100644 index 00000000..7f9ffbe1 --- /dev/null +++ b/src/snapshot/Serialize/IndentedOutput.spec.lua @@ -0,0 +1,72 @@ +return function() + local IndentedOutput = require(script.Parent.IndentedOutput) + + describe("join", function() + it("should concat the lines with a new line by default", function() + local output = IndentedOutput.new() + + output:write("foo") + output:write("bar") + + expect(output:join()).to.equal("foo\nbar") + end) + + it("should concat the lines with the given string", function() + local output = IndentedOutput.new() + + output:write("foo") + output:write("bar") + + expect(output:join("-")).to.equal("foo-bar") + end) + end) + + describe("push", function() + it("should indent next written lines", function() + local output = IndentedOutput.new() + + output:write("foo") + output:push() + output:write("bar") + + expect(output:join()).to.equal("foo\n bar") + end) + end) + + describe("pop", function() + it("should dedent next written lines", function() + local output = IndentedOutput.new() + + output:write("foo") + output:push() + output:write("bar") + output:pop() + output:write("baz") + + expect(output:join()).to.equal("foo\n bar\nbaz") + end) + end) + + describe("writeAndPush", function() + it("should write the line and push", function() + local output = IndentedOutput.new() + + output:writeAndPush("foo") + output:write("bar") + + expect(output:join()).to.equal("foo\n bar") + end) + end) + + describe("popAndWrite", function() + it("should write the line and push", function() + local output = IndentedOutput.new() + + output:writeAndPush("foo") + output:write("bar") + output:popAndWrite("baz") + + expect(output:join()).to.equal("foo\n bar\nbaz") + end) + end) +end \ No newline at end of file diff --git a/src/snapshot/Serialize/Serializer.lua b/src/snapshot/Serialize/Serializer.lua new file mode 100644 index 00000000..3bb7ce63 --- /dev/null +++ b/src/snapshot/Serialize/Serializer.lua @@ -0,0 +1,189 @@ +local AnonymousFunction = require(script.Parent.AnonymousFunction) +local ElementKind = require(script.Parent.Parent.Parent.ElementKind) +local IndentedOutput = require(script.Parent.IndentedOutput) +local Type = require(script.Parent.Parent.Parent.Type) + +local function sortRoactEvents(a, b) + return a.name < b.name +end + +local Serializer = {} + +function Serializer.kind(kind) + if kind == ElementKind.Host then + return "Host" + elseif kind == ElementKind.Function then + return "Function" + elseif kind == ElementKind.Stateful then + return "Stateful" + else + error(("Cannot serialize ElementKind %q"):format(tostring(kind))) + end +end + +function Serializer.type(data, output) + output:writeAndPush("type = {") + output:write("kind = ElementKind.%s,", Serializer.kind(data.kind)) + + if data.className then + output:write("className = %q,", data.className) + elseif data.componentName then + output:write("componentName = %q,", data.componentName) + end + + output:popAndWrite("},") +end + +function Serializer.propKey(key) + if key:match("^%a%w+$") then + return key + else + return ("[%q]"):format(key) + end +end + +function Serializer.propValue(prop) + local propType = typeof(prop) + + if propType == "string" then + return ("%q"):format(prop) + + elseif propType == "number" or propType == "boolean" then + return ("%s"):format(tostring(prop)) + + elseif propType == "Color3" then + return ("Color3.new(%s, %s, %s)"):format(prop.r, prop.g, prop.b) + + elseif propType == "EnumItem" then + return ("%s"):format(tostring(prop)) + + elseif propType == "UDim" then + return ("UDim.new(%s, %s)"):format(prop.Scale, prop.Offset) + + elseif propType == "UDim2" then + return ("UDim2.new(%s, %s, %s, %s)"):format( + prop.X.Scale, + prop.X.Offset, + prop.Y.Scale, + prop.Y.Offset + ) + + elseif propType == "Vector2" then + return ("Vector2.new(%s, %s)"):format(prop.X, prop.Y) + + elseif prop == AnonymousFunction then + return "AnonymousFunction" + + else + error(("Cannot serialize prop %q with value of type %q"):format( + tostring(prop), + propType + )) + end +end + +function Serializer.tableContent(dict, output) + local keys = {} + + for key in pairs(dict) do + table.insert(keys, key) + end + + table.sort(keys) + + for i=1, #keys do + local key = keys[i] + output:write("%s = %s,", Serializer.propKey(key), Serializer.propValue(dict[key], output)) + end +end + +function Serializer.props(props, output) + if next(props) == nil then + output:write("props = {},") + return + end + + local stringProps = {} + local events = {} + local changedEvents = {} + + output:writeAndPush("props = {") + + for key, value in pairs(props) do + if type(key) == "string" then + stringProps[key] = value + + elseif Type.of(key) == Type.HostEvent then + table.insert(events, key) + + elseif Type.of(key) == Type.HostChangeEvent then + table.insert(changedEvents, key) + + end + end + + Serializer.tableContent(stringProps, output) + table.sort(events, sortRoactEvents) + table.sort(changedEvents, sortRoactEvents) + + for i=1, #events do + local event = events[i] + local serializedPropValue = Serializer.propValue(props[event], output) + output:write("[Roact.Event.%s] = %s,", event.name, serializedPropValue) + end + + for i=1, #changedEvents do + local changedEvent = changedEvents[i] + local serializedPropValue = Serializer.propValue(props[changedEvent], output) + output:write("[Roact.Change.%s] = %s,", changedEvent.name, serializedPropValue) + end + + output:popAndWrite("},") +end + +function Serializer.children(children, output) + if #children == 0 then + output:write("children = {},") + return + end + + output:writeAndPush("children = {") + + for i=1, #children do + Serializer.snapshotData(children[i], output) + end + + output:popAndWrite("},") +end + +function Serializer.snapshotDataContent(snapshotData, output) + Serializer.type(snapshotData.type, output) + output:write("hostKey = %q,", snapshotData.hostKey) + Serializer.props(snapshotData.props, output) + Serializer.children(snapshotData.children, output) +end + +function Serializer.snapshotData(snapshotData, output) + output:writeAndPush("{") + Serializer.snapshotDataContent(snapshotData, output) + output:popAndWrite("},") +end + +function Serializer.firstSnapshotData(snapshotData) + local output = IndentedOutput.new() + output:writeAndPush("return function(dependencies)") + output:write("local Roact = dependencies.Roact") + output:write("local ElementKind = dependencies.ElementKind") + output:write("local AnonymousFunction = dependencies.AnonymousFunction") + output:write("") + output:writeAndPush("return {") + + Serializer.snapshotDataContent(snapshotData, output) + + output:popAndWrite("}") + output:popAndWrite("end") + + return output:join() +end + +return Serializer diff --git a/src/snapshot/Serialize/Serializer.spec.lua b/src/snapshot/Serialize/Serializer.spec.lua new file mode 100644 index 00000000..b5cec7e9 --- /dev/null +++ b/src/snapshot/Serialize/Serializer.spec.lua @@ -0,0 +1,274 @@ +return function() + local AnonymousFunction = require(script.Parent.AnonymousFunction) + local Change = require(script.Parent.Parent.Parent.PropMarkers.Change) + local Event = require(script.Parent.Parent.Parent.PropMarkers.Event) + local ElementKind = require(script.Parent.Parent.Parent.ElementKind) + local IndentedOutput = require(script.Parent.IndentedOutput) + local Serializer = require(script.Parent.Serializer) + + describe("type", function() + it("should serialize host elements", function() + local output = IndentedOutput.new() + Serializer.type({ + kind = ElementKind.Host, + className = "TextLabel", + }, output) + + expect(output:join()).to.equal( + "type = {\n" + .. " kind = ElementKind.Host,\n" + .. " className = \"TextLabel\",\n" + .. "}," + ) + end) + + it("should serialize stateful elements", function() + local output = IndentedOutput.new() + Serializer.type({ + kind = ElementKind.Stateful, + componentName = "SomeComponent", + }, output) + + expect(output:join()).to.equal( + "type = {\n" + .. " kind = ElementKind.Stateful,\n" + .. " componentName = \"SomeComponent\",\n" + .. "}," + ) + end) + + it("should serialize function elements", function() + local output = IndentedOutput.new() + Serializer.type({ + kind = ElementKind.Function, + }, output) + + expect(output:join()).to.equal( + "type = {\n" + .. " kind = ElementKind.Function,\n" + .. "}," + ) + end) + end) + + describe("propKey", function() + it("should serialize to a named dictionary field", function() + local keys = {"foo", "foo1"} + + for i=1, #keys do + local key = keys[i] + local result = Serializer.propKey(key) + + expect(result).to.equal(key) + end + end) + + it("should serialize to a string value field to escape non-alphanumeric characters", function() + local keys = {"foo.bar", "1foo"} + + for i=1, #keys do + local key = keys[i] + local result = Serializer.propKey(key) + + expect(result).to.equal('["' .. key .. '"]') + end + end) + end) + + describe("propValue", function() + it("should serialize strings", function() + local result = Serializer.propValue("foo") + + expect(result).to.equal('"foo"') + end) + + it("should serialize strings with \"", function() + local result = Serializer.propValue('foo"bar') + + expect(result).to.equal('"foo\\"bar"') + end) + + it("should serialize numbers", function() + local result = Serializer.propValue(10.5) + + expect(result).to.equal("10.5") + end) + + it("should serialize booleans", function() + expect(Serializer.propValue(true)).to.equal("true") + expect(Serializer.propValue(false)).to.equal("false") + end) + + it("should serialize enum items", function() + local result = Serializer.propValue(Enum.SortOrder.LayoutOrder) + + expect(result).to.equal("Enum.SortOrder.LayoutOrder") + end) + + it("should serialize Color3", function() + local result = Serializer.propValue(Color3.new(0.1, 0.2, 0.3)) + + expect(result).to.equal("Color3.new(0.1, 0.2, 0.3)") + end) + + it("should serialize UDim", function() + local result = Serializer.propValue(UDim.new(1, 0.5)) + + expect(result).to.equal("UDim.new(1, 0.5)") + end) + + it("should serialize UDim2", function() + local result = Serializer.propValue(UDim2.new(1, 0.5, 2, 2.5)) + + expect(result).to.equal("UDim2.new(1, 0.5, 2, 2.5)") + end) + + it("should serialize Vector2", function() + local result = Serializer.propValue(Vector2.new(1.5, 0.3)) + + expect(result).to.equal("Vector2.new(1.5, 0.3)") + end) + + it("should serialize AnonymousFunction symbol", function() + local result = Serializer.propValue(AnonymousFunction) + + expect(result).to.equal("AnonymousFunction") + end) + end) + + describe("props", function() + it("should serialize an empty table", function() + local output = IndentedOutput.new() + Serializer.props({}, output) + + expect(output:join()).to.equal("props = {},") + end) + + it("should serialize table fields", function() + local output = IndentedOutput.new() + Serializer.props({ + key = 8, + }, output) + + expect(output:join()).to.equal("props = {\n key = 8,\n},") + end) + + it("should serialize Roact.Event", function() + local output = IndentedOutput.new() + Serializer.props({ + [Event.Activated] = AnonymousFunction, + }, output) + + expect(output:join()).to.equal( + "props = {\n" + .. " [Roact.Event.Activated] = AnonymousFunction,\n" + .. "}," + ) + end) + + it("should serialize Roact.Change", function() + local output = IndentedOutput.new() + Serializer.props({ + [Change.Position] = AnonymousFunction, + }, output) + + expect(output:join()).to.equal( + "props = {\n" + .. " [Roact.Change.Position] = AnonymousFunction,\n" + .. "}," + ) + end) + end) + + describe("children", function() + it("should serialize an empty table", function() + local output = IndentedOutput.new() + Serializer.children({}, output) + + expect(output:join()).to.equal("children = {},") + end) + + it("should serialize children in an array", function() + local snapshotData = { + type = { + kind = ElementKind.Function, + }, + hostKey = "HostKey", + props = {}, + children = {}, + } + + local childrenOutput = IndentedOutput.new() + Serializer.children({snapshotData}, childrenOutput) + + local snapshotDataOutput = IndentedOutput.new() + snapshotDataOutput:push() + Serializer.snapshotData(snapshotData, snapshotDataOutput) + + local expectResult = "children = {\n" .. snapshotDataOutput:join() .. "\n}," + expect(childrenOutput:join()).to.equal(expectResult) + end) + end) + + describe("snapshotDataContent", function() + it("should serialize all fields", function() + local snapshotData = { + type = { + kind = ElementKind.Function, + }, + hostKey = "HostKey", + props = {}, + children = {}, + } + local output = IndentedOutput.new() + Serializer.snapshotDataContent(snapshotData, output) + + expect(output:join()).to.equal( + "type = {\n" + .. " kind = ElementKind.Function,\n" + .. "},\n" + .. 'hostKey = "HostKey",\n' + .. "props = {},\n" + .. "children = {}," + ) + end) + end) + + describe("snapshotData", function() + it("should wrap snapshotDataContent result between curly braces", function() + local snapshotData = { + type = { + kind = ElementKind.Function, + }, + hostKey = "HostKey", + props = {}, + children = {}, + } + local contentOutput = IndentedOutput.new() + contentOutput:push() + Serializer.snapshotDataContent(snapshotData, contentOutput) + + local output = IndentedOutput.new() + Serializer.snapshotData(snapshotData, output) + + local expectResult = "{\n" .. contentOutput:join() .. "\n}," + expect(output:join()).to.equal(expectResult) + end) + end) + + describe("firstSnapshotData", function() + it("should return a function that returns a table", function() + local result = Serializer.firstSnapshotData({ + type = { + kind = ElementKind.Function, + }, + hostKey = "HostKey", + props = {}, + children = {}, + }) + + local pattern = "^return function%(.-%).+return%s+{(.+)}%s+end$" + expect(result:match(pattern)).to.be.ok() + end) + end) +end \ No newline at end of file diff --git a/src/snapshot/Serialize/SnapshotData.lua b/src/snapshot/Serialize/SnapshotData.lua new file mode 100644 index 00000000..cbbb7f37 --- /dev/null +++ b/src/snapshot/Serialize/SnapshotData.lua @@ -0,0 +1,90 @@ +local AnonymousFunction = require(script.Parent.AnonymousFunction) +local ElementKind = require(script.Parent.Parent.Parent.ElementKind) +local Type = require(script.Parent.Parent.Parent.Type) + +local function sortSerializedChildren(childA, childB) + return childA.hostKey < childB.hostKey +end + +local SnapshotData = {} + +function SnapshotData.type(wrapperType) + local typeData = { + kind = wrapperType.kind, + } + + if wrapperType.kind == ElementKind.Host then + typeData.className = wrapperType.className + elseif wrapperType.kind == ElementKind.Stateful then + typeData.componentName = tostring(wrapperType.component) + end + + return typeData +end + +function SnapshotData.propValue(prop) + local propType = type(prop) + + if propType == "string" + or propType == "number" + or propType == "boolean" + or propType == "userdata" + then + return prop + + elseif propType == 'function' then + return AnonymousFunction + + else + error(("SnapshotData does not support prop with value %q (type %q)"):format( + tostring(prop), + propType + )) + end +end + +function SnapshotData.props(wrapperProps) + local serializedProps = {} + + for key, prop in pairs(wrapperProps) do + if type(key) == "string" + or Type.of(key) == Type.HostChangeEvent + or Type.of(key) == Type.HostEvent + then + serializedProps[key] = SnapshotData.propValue(prop) + + else + error(("SnapshotData does not support prop with key %q (type: %s)"):format( + tostring(key), + type(key) + )) + end + end + + return serializedProps +end + +function SnapshotData.children(children) + local serializedChildren = {} + + for i=1, #children do + local childWrapper = children[i] + + serializedChildren[i] = SnapshotData.wrapper(childWrapper) + end + + table.sort(serializedChildren, sortSerializedChildren) + + return serializedChildren +end + +function SnapshotData.wrapper(wrapper) + return { + type = SnapshotData.type(wrapper.type), + hostKey = wrapper.hostKey, + props = SnapshotData.props(wrapper.props), + children = SnapshotData.children(wrapper:getChildren()), + } +end + +return SnapshotData diff --git a/src/snapshot/Serialize/SnapshotData.spec.lua b/src/snapshot/Serialize/SnapshotData.spec.lua new file mode 100644 index 00000000..19d59262 --- /dev/null +++ b/src/snapshot/Serialize/SnapshotData.spec.lua @@ -0,0 +1,208 @@ +return function() + local RoactRoot = script.Parent.Parent.Parent + + local AnonymousFunction = require(script.Parent.AnonymousFunction) + local assertDeepEqual = require(RoactRoot.assertDeepEqual) + local Change = require(RoactRoot.PropMarkers.Change) + local Component = require(RoactRoot.Component) + local createElement = require(RoactRoot.createElement) + local ElementKind = require(RoactRoot.ElementKind) + local Event = require(RoactRoot.PropMarkers.Event) + local shallow = require(RoactRoot.shallow) + + local SnapshotData = require(script.Parent.SnapshotData) + + describe("type", function() + describe("host elements", function() + it("should contain the host kind", function() + local wrapper = shallow(createElement("Frame")) + + local result = SnapshotData.type(wrapper.type) + + expect(result.kind).to.equal(ElementKind.Host) + end) + + it("should contain the class name", function() + local className = "Frame" + local wrapper = shallow(createElement(className)) + + local result = SnapshotData.type(wrapper.type) + + expect(result.className).to.equal(className) + end) + end) + + describe("function elements", function() + local function SomeComponent() + return nil + end + + it("should contain the host kind", function() + local wrapper = shallow(createElement(SomeComponent)) + + local result = SnapshotData.type(wrapper.type) + + expect(result.kind).to.equal(ElementKind.Function) + end) + end) + + describe("stateful elements", function() + local componentName = "ComponentName" + local SomeComponent = Component:extend(componentName) + + function SomeComponent:render() + return nil + end + + it("should contain the host kind", function() + local wrapper = shallow(createElement(SomeComponent)) + + local result = SnapshotData.type(wrapper.type) + + expect(result.kind).to.equal(ElementKind.Stateful) + end) + + it("should contain the component name", function() + local wrapper = shallow(createElement(SomeComponent)) + + local result = SnapshotData.type(wrapper.type) + + expect(result.componentName).to.equal(componentName) + end) + end) + end) + + describe("propValue", function() + it("should return the same value", function() + local propValues = {7, "hello", Enum.SortOrder.LayoutOrder} + + for i=1, #propValues do + local prop = propValues[i] + local result = SnapshotData.propValue(prop) + + expect(result).to.equal(prop) + end + end) + + it("should return the AnonymousFunction symbol when given a function", function() + local result = SnapshotData.propValue(function() end) + + expect(result).to.equal(AnonymousFunction) + end) + end) + + describe("props", function() + it("should keep props with string keys", function() + local props = { + image = "hello", + text = "never", + } + + local result = SnapshotData.props(props) + + assertDeepEqual(result, props) + end) + + it("should map Roact.Event to AnonymousFunction", function() + local props = { + [Event.Activated] = function() end, + } + + local result = SnapshotData.props(props) + + assertDeepEqual(result, { + [Event.Activated] = AnonymousFunction, + }) + end) + + it("should map Roact.Change to AnonymousFunction", function() + local props = { + [Change.Position] = function() end, + } + + local result = SnapshotData.props(props) + + assertDeepEqual(result, { + [Change.Position] = AnonymousFunction, + }) + end) + + it("should throw when the key is a table", function() + local function shouldThrow() + SnapshotData.props({ + [{}] = "invalid", + }) + end + + expect(shouldThrow).to.throw() + end) + end) + + describe("wrapper", function() + it("should have the host key", function() + local hostKey = "SomeKey" + local wrapper = shallow(createElement("Frame")) + wrapper.hostKey = hostKey + + local result = SnapshotData.wrapper(wrapper) + + expect(result.hostKey).to.equal(hostKey) + end) + + it("should contain the element type", function() + local wrapper = shallow(createElement("Frame")) + + local result = SnapshotData.wrapper(wrapper) + + expect(result.type).to.be.ok() + expect(result.type.kind).to.equal(ElementKind.Host) + expect(result.type.className).to.equal("Frame") + end) + + it("should contain the props", function() + local props = { + LayoutOrder = 3, + [Change.Size] = function() end, + } + local expectProps = { + LayoutOrder = 3, + [Change.Size] = AnonymousFunction, + } + + local wrapper = shallow(createElement("Frame", props)) + + local result = SnapshotData.wrapper(wrapper) + + expect(result.props).to.be.ok() + assertDeepEqual(result.props, expectProps) + end) + + it("should contain the element children", function() + local wrapper = shallow(createElement("Frame", {}, { + Child = createElement("TextLabel"), + })) + + local result = SnapshotData.wrapper(wrapper) + + expect(result.children).to.be.ok() + expect(#result.children).to.equal(1) + local childData = result.children[1] + expect(childData.type.kind).to.equal(ElementKind.Host) + expect(childData.type.className).to.equal("TextLabel") + end) + + it("should sort children by their host key", function() + local wrapper = shallow(createElement("Frame", {}, { + Child = createElement("TextLabel"), + Label = createElement("TextLabel"), + })) + + local result = SnapshotData.wrapper(wrapper) + + expect(result.children).to.be.ok() + expect(#result.children).to.equal(2) + expect(result.children[1].hostKey).to.equal("Child") + expect(result.children[2].hostKey).to.equal("Label") + end) + end) +end \ No newline at end of file diff --git a/src/snapshot/Serialize/init.lua b/src/snapshot/Serialize/init.lua new file mode 100644 index 00000000..a63178ad --- /dev/null +++ b/src/snapshot/Serialize/init.lua @@ -0,0 +1,11 @@ +local Serializer = require(script.Serializer) +local SnapshotData = require(script.SnapshotData) + +return { + wrapperToSnapshotData = function(wrapper) + return SnapshotData.wrapper(wrapper) + end, + snapshotDataToString = function(data) + return Serializer.firstSnapshotData(data) + end, +} diff --git a/src/snapshot/Snapshot.lua b/src/snapshot/Snapshot.lua new file mode 100644 index 00000000..c42b8012 --- /dev/null +++ b/src/snapshot/Snapshot.lua @@ -0,0 +1,97 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local AnonymousFunction = require(script.Parent.Serialize.AnonymousFunction) +local Serialize = require(script.Parent.Serialize) +local deepEqual = require(script.Parent.Parent.deepEqual) +local ElementKind = require(script.Parent.Parent.ElementKind) + +local SnapshotFolderName = "RoactSnapshots" +local SnapshotFolder = ReplicatedStorage:FindFirstChild(SnapshotFolderName) + +local Snapshot = {} +local SnapshotMetatable = { + __index = Snapshot, + __tostring = function(snapshot) + return Serialize.snapshotDataToString(snapshot.data) + end +} + +function Snapshot.new(identifier, data) + local snapshot = { + _identifier = identifier, + data = data, + _existingData = Snapshot._loadExistingData(identifier), + } + + setmetatable(snapshot, SnapshotMetatable) + + return snapshot +end + +function Snapshot:match() + if self._existingData == nil then + self:serialize() + self._existingData = self.data + return + end + + local areEqual, innerMessageTemplate = deepEqual(self.data, self._existingData) + + if areEqual then + return + end + + local innerMessage = innerMessageTemplate + :gsub("{1}", "new") + :gsub("{2}", "existing") + + local message = ("Snapshots do not match.\n%s"):format(innerMessage) + + error(message, 2) +end + +function Snapshot:serialize() + local folder = Snapshot.getSnapshotFolder() + + local existingData = folder:FindFirstChild(self._identifier) + + if not existingData then + existingData = Instance.new("StringValue") + existingData.Name = self._identifier + existingData.Parent = folder + end + + existingData.Value = Serialize.snapshotDataToString(self.data) +end + +function Snapshot.getSnapshotFolder() + SnapshotFolder = ReplicatedStorage:FindFirstChild(SnapshotFolderName) + + if not SnapshotFolder then + SnapshotFolder = Instance.new("Folder") + SnapshotFolder.Name = SnapshotFolderName + SnapshotFolder.Parent = ReplicatedStorage + end + + return SnapshotFolder +end + +function Snapshot._loadExistingData(identifier) + local folder = Snapshot.getSnapshotFolder() + + local existingData = folder:FindFirstChild(identifier) + + if not (existingData and existingData:IsA("ModuleScript")) then + return nil + end + + local loadSnapshot = require(existingData) + + return loadSnapshot({ + Roact = require(script.Parent.Parent), + ElementKind = ElementKind, + AnonymousFunction = AnonymousFunction, + }) +end + +return Snapshot \ No newline at end of file diff --git a/src/snapshot/Snapshot.spec.lua b/src/snapshot/Snapshot.spec.lua new file mode 100644 index 00000000..2453f827 --- /dev/null +++ b/src/snapshot/Snapshot.spec.lua @@ -0,0 +1,132 @@ +return function() + local Snapshot = require(script.Parent.Snapshot) + + local ElementKind = require(script.Parent.Parent.ElementKind) + local createSpy = require(script.Parent.Parent.createSpy) + + local snapshotFolder = Instance.new("Folder") + local originalGetSnapshotFolder = Snapshot.getSnapshotFolder + Snapshot.getSnapshotFolder = function() + return snapshotFolder + end + + local originalLoadExistingData = Snapshot._loadExistingData + local loadExistingDataSpy = nil + + describe("match", function() + local snapshotMap = {} + + local function beforeTest() + snapshotMap = {} + + loadExistingDataSpy = createSpy(function(identifier) + return snapshotMap[identifier] + end) + Snapshot._loadExistingData = loadExistingDataSpy.value + end + + local function cleanTest() + loadExistingDataSpy = nil + Snapshot._loadExistingData = originalLoadExistingData + end + + it("should serialize the snapshot if no data is found", function() + beforeTest() + + local data = {} + + local serializeSpy = createSpy() + + local snapshot = Snapshot.new("foo", data) + snapshot.serialize = serializeSpy.value + + snapshot:match() + + cleanTest() + + serializeSpy:assertCalledWith(snapshot) + end) + + it("should not serialize if the snapshot already exist", function() + beforeTest() + + local data = {} + local identifier = "foo" + snapshotMap[identifier] = data + + local serializeSpy = createSpy() + + local snapshot = Snapshot.new(identifier, data) + snapshot.serialize = serializeSpy.value + + snapshot:match() + + cleanTest() + + expect(serializeSpy.callCount).to.equal(0) + end) + + it("should throw an error if the previous snapshot does not match", function() + beforeTest() + + local data = {} + local identifier = "foo" + snapshotMap[identifier] = { + Key = "Value" + } + + local serializeSpy = createSpy() + + local snapshot = Snapshot.new(identifier, data) + snapshot.serialize = serializeSpy.value + + local function shouldThrow() + snapshot:match() + end + + cleanTest() + + expect(shouldThrow).to.throw() + end) + end) + + describe("serialize", function() + it("should create a StringValue if it does not exist", function() + local identifier = "foo" + + local snapshot = Snapshot.new(identifier, { + type = { + kind = ElementKind.Function, + }, + hostKey = "HostKey", + props = {}, + children = {}, + }) + + snapshot:serialize() + local stringValue = snapshotFolder:FindFirstChild(identifier) + + expect(stringValue).to.be.ok() + expect(stringValue.Value:len() > 0).to.equal(true) + end) + end) + + describe("_loadExistingData", function() + it("should return nil if data is not found", function() + local result = Snapshot._loadExistingData("foo") + + expect(result).never.to.be.ok() + end) + end) + + describe("getSnapshotFolder", function() + it("should create a folder in the ReplicatedStorage if it is not found", function() + local folder = originalGetSnapshotFolder() + + expect(folder).to.be.ok() + expect(folder.Parent).to.equal(game:GetService("ReplicatedStorage")) + + folder:Destroy() + end) + end) +end \ No newline at end of file diff --git a/src/snapshot/init.lua b/src/snapshot/init.lua new file mode 100644 index 00000000..d8a568cf --- /dev/null +++ b/src/snapshot/init.lua @@ -0,0 +1,9 @@ +local Serialize = require(script.Serialize) +local Snapshot = require(script.Snapshot) + +return function(identifier, shallowWrapper) + local data = Serialize.wrapperToSnapshotData(shallowWrapper) + local snapshot = Snapshot.new(identifier, data) + + return snapshot +end From 17d8146e60f1ea8ad2b2c3475fc4538dd428b83b Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 23 Jul 2019 14:55:10 -0700 Subject: [PATCH 08/65] Add ref serialization --- src/snapshot/Serialize/IndentedOutput.lua | 8 +- .../{ => Markers}/AnonymousFunction.lua | 2 +- src/snapshot/Serialize/Markers/EmptyRef.lua | 5 + src/snapshot/Serialize/Markers/Signal.lua | 5 + src/snapshot/Serialize/Markers/Unknown.lua | 5 + src/snapshot/Serialize/Markers/init.lua | 8 + src/snapshot/Serialize/Serializer.lua | 171 ++++++++++-------- src/snapshot/Serialize/Serializer.spec.lua | 108 ++++++++--- src/snapshot/Serialize/SnapshotData.lua | 44 ++++- src/snapshot/Serialize/SnapshotData.spec.lua | 66 ++++++- src/snapshot/Snapshot.lua | 7 +- src/snapshot/init.lua | 8 + 12 files changed, 322 insertions(+), 115 deletions(-) rename src/snapshot/Serialize/{ => Markers}/AnonymousFunction.lua (56%) create mode 100644 src/snapshot/Serialize/Markers/EmptyRef.lua create mode 100644 src/snapshot/Serialize/Markers/Signal.lua create mode 100644 src/snapshot/Serialize/Markers/Unknown.lua create mode 100644 src/snapshot/Serialize/Markers/init.lua diff --git a/src/snapshot/Serialize/IndentedOutput.lua b/src/snapshot/Serialize/IndentedOutput.lua index cfef8376..c4ccd3ae 100644 --- a/src/snapshot/Serialize/IndentedOutput.lua +++ b/src/snapshot/Serialize/IndentedOutput.lua @@ -35,14 +35,14 @@ function IndentedOutput:pop() self._level = math.max(self._level - 1, 0) end -function IndentedOutput:writeAndPush(line) - self:write(line) +function IndentedOutput:writeAndPush(...) + self:write(...) self:push() end -function IndentedOutput:popAndWrite(line) +function IndentedOutput:popAndWrite(...) self:pop() - self:write(line) + self:write(...) end function IndentedOutput:join(separator) diff --git a/src/snapshot/Serialize/AnonymousFunction.lua b/src/snapshot/Serialize/Markers/AnonymousFunction.lua similarity index 56% rename from src/snapshot/Serialize/AnonymousFunction.lua rename to src/snapshot/Serialize/Markers/AnonymousFunction.lua index 62be7341..a5c1abca 100644 --- a/src/snapshot/Serialize/AnonymousFunction.lua +++ b/src/snapshot/Serialize/Markers/AnonymousFunction.lua @@ -1,4 +1,4 @@ -local Symbol = require(script.Parent.Parent.Parent.Symbol) +local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) local AnonymousFunction = Symbol.named("AnonymousFunction") diff --git a/src/snapshot/Serialize/Markers/EmptyRef.lua b/src/snapshot/Serialize/Markers/EmptyRef.lua new file mode 100644 index 00000000..cf5e193f --- /dev/null +++ b/src/snapshot/Serialize/Markers/EmptyRef.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) + +local EmptyRef = Symbol.named("EmptyRef") + +return EmptyRef \ No newline at end of file diff --git a/src/snapshot/Serialize/Markers/Signal.lua b/src/snapshot/Serialize/Markers/Signal.lua new file mode 100644 index 00000000..9e9285b0 --- /dev/null +++ b/src/snapshot/Serialize/Markers/Signal.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) + +local Signal = Symbol.named("Signal") + +return Signal \ No newline at end of file diff --git a/src/snapshot/Serialize/Markers/Unknown.lua b/src/snapshot/Serialize/Markers/Unknown.lua new file mode 100644 index 00000000..24f8c48d --- /dev/null +++ b/src/snapshot/Serialize/Markers/Unknown.lua @@ -0,0 +1,5 @@ +local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) + +local Unkown = Symbol.named("Unkown") + +return Unkown \ No newline at end of file diff --git a/src/snapshot/Serialize/Markers/init.lua b/src/snapshot/Serialize/Markers/init.lua new file mode 100644 index 00000000..624661d5 --- /dev/null +++ b/src/snapshot/Serialize/Markers/init.lua @@ -0,0 +1,8 @@ +local strict = require(script.Parent.Parent.Parent.strict) + +return strict({ + AnonymousFunction = require(script.AnonymousFunction), + EmptyRef = require(script.EmptyRef), + Signal = require(script.Signal), + Unknown = require(script.Unknown), +}, "Markers") \ No newline at end of file diff --git a/src/snapshot/Serialize/Serializer.lua b/src/snapshot/Serialize/Serializer.lua index 3bb7ce63..7f07522f 100644 --- a/src/snapshot/Serialize/Serializer.lua +++ b/src/snapshot/Serialize/Serializer.lua @@ -1,11 +1,9 @@ -local AnonymousFunction = require(script.Parent.AnonymousFunction) -local ElementKind = require(script.Parent.Parent.Parent.ElementKind) +local RoactRoot = script.Parent.Parent.Parent +local ElementKind = require(RoactRoot.ElementKind) +local Ref = require(RoactRoot.PropMarkers.Ref) +local Type = require(RoactRoot.Type) +local Markers = require(script.Parent.Markers) local IndentedOutput = require(script.Parent.IndentedOutput) -local Type = require(script.Parent.Parent.Parent.Type) - -local function sortRoactEvents(a, b) - return a.name < b.name -end local Serializer = {} @@ -34,113 +32,140 @@ function Serializer.type(data, output) output:popAndWrite("},") end -function Serializer.propKey(key) - if key:match("^%a%w+$") then +function Serializer.tableKey(key) + local keyType = type(key) + + if keyType == "string" and key:match("^%a%w+$") then return key else - return ("[%q]"):format(key) + return ("[%s]"):format(Serializer.tableValue(key)) end end -function Serializer.propValue(prop) - local propType = typeof(prop) +function Serializer.tableValue(value) + local valueType = typeof(value) - if propType == "string" then - return ("%q"):format(prop) + if valueType == "string" then + return ("%q"):format(value) - elseif propType == "number" or propType == "boolean" then - return ("%s"):format(tostring(prop)) + elseif valueType == "number" or valueType == "boolean" then + return ("%s"):format(tostring(value)) - elseif propType == "Color3" then - return ("Color3.new(%s, %s, %s)"):format(prop.r, prop.g, prop.b) + elseif valueType == "Color3" then + return ("Color3.new(%s, %s, %s)"):format(value.r, value.g, value.b) - elseif propType == "EnumItem" then - return ("%s"):format(tostring(prop)) + elseif valueType == "EnumItem" then + return ("%s"):format(tostring(value)) - elseif propType == "UDim" then - return ("UDim.new(%s, %s)"):format(prop.Scale, prop.Offset) + elseif valueType == "UDim" then + return ("UDim.new(%s, %s)"):format(value.Scale, value.Offset) - elseif propType == "UDim2" then + elseif valueType == "UDim2" then return ("UDim2.new(%s, %s, %s, %s)"):format( - prop.X.Scale, - prop.X.Offset, - prop.Y.Scale, - prop.Y.Offset + value.X.Scale, + value.X.Offset, + value.Y.Scale, + value.Y.Offset ) - elseif propType == "Vector2" then - return ("Vector2.new(%s, %s)"):format(prop.X, prop.Y) + elseif valueType == "Vector2" then + return ("Vector2.new(%s, %s)"):format(value.X, value.Y) + + elseif Type.of(value) == Type.HostEvent then + return ("Roact.Event.%s"):format(value.name) + + elseif Type.of(value) == Type.HostChangeEvent then + return ("Roact.Change.%s"):format(value.name) - elseif prop == AnonymousFunction then - return "AnonymousFunction" + elseif value == Ref then + return "Roact.Ref" else - error(("Cannot serialize prop %q with value of type %q"):format( - tostring(prop), - propType + for markerName, marker in pairs(Markers) do + if value == marker then + return ("Markers.%s"):format(markerName) + end + end + + error(("Cannot serialize value %q of type %q"):format( + tostring(value), + valueType )) end end -function Serializer.tableContent(dict, output) - local keys = {} +function Serializer.getKeyTypeOrder(key) + if type(key) == "string" then + return 1 + elseif Type.of(key) == Type.HostEvent then + return 2 + elseif Type.of(key) == Type.HostChangeEvent then + return 3 + elseif key == Ref then + return 4 + else + return math.huge + end +end - for key in pairs(dict) do - table.insert(keys, key) +function Serializer.compareKeys(a, b) + -- a and b are of the same type here, because Serializer.sortTableKeys + -- will only use this function to compare keys of the same type + if Type.of(a) == Type.HostEvent or Type.of(a) == Type.HostChangeEvent then + return a.name < b.name + else + return a < b end +end - table.sort(keys) +function Serializer.sortTableKeys(a, b) + -- first sort by the type of key, to place string props, then Roact.Event + -- events, Roact.Change events and the Ref + local orderA = Serializer.getKeyTypeOrder(a) + local orderB = Serializer.getKeyTypeOrder(b) - for i=1, #keys do - local key = keys[i] - output:write("%s = %s,", Serializer.propKey(key), Serializer.propValue(dict[key], output)) + if orderA == orderB then + return Serializer.compareKeys(a, b) + else + return orderA < orderB end end -function Serializer.props(props, output) - if next(props) == nil then - output:write("props = {},") +function Serializer.table(tableKey, dict, output) + if next(dict) == nil then + output:write("%s = {},", tableKey) return end - local stringProps = {} - local events = {} - local changedEvents = {} - - output:writeAndPush("props = {") - - for key, value in pairs(props) do - if type(key) == "string" then - stringProps[key] = value + output:writeAndPush("%s = {", tableKey) - elseif Type.of(key) == Type.HostEvent then - table.insert(events, key) - - elseif Type.of(key) == Type.HostChangeEvent then - table.insert(changedEvents, key) + local keys = {} - end + for key in pairs(dict) do + table.insert(keys, key) end - Serializer.tableContent(stringProps, output) - table.sort(events, sortRoactEvents) - table.sort(changedEvents, sortRoactEvents) + table.sort(keys, Serializer.sortTableKeys) - for i=1, #events do - local event = events[i] - local serializedPropValue = Serializer.propValue(props[event], output) - output:write("[Roact.Event.%s] = %s,", event.name, serializedPropValue) - end + for i=1, #keys do + local key = keys[i] + local value = dict[key] + local serializedKey = Serializer.tableKey(key) - for i=1, #changedEvents do - local changedEvent = changedEvents[i] - local serializedPropValue = Serializer.propValue(props[changedEvent], output) - output:write("[Roact.Change.%s] = %s,", changedEvent.name, serializedPropValue) + if type(value) == "table" then + Serializer.table(serializedKey, value, output) + else + output:write("%s = %s,", serializedKey, Serializer.tableValue(value)) + end end output:popAndWrite("},") end +function Serializer.props(props, output) + Serializer.table("props", props, output) +end + function Serializer.children(children, output) if #children == 0 then output:write("children = {},") @@ -174,7 +199,7 @@ function Serializer.firstSnapshotData(snapshotData) output:writeAndPush("return function(dependencies)") output:write("local Roact = dependencies.Roact") output:write("local ElementKind = dependencies.ElementKind") - output:write("local AnonymousFunction = dependencies.AnonymousFunction") + output:write("local Markers = dependencies.Markers") output:write("") output:writeAndPush("return {") diff --git a/src/snapshot/Serialize/Serializer.spec.lua b/src/snapshot/Serialize/Serializer.spec.lua index b5cec7e9..91f69a0b 100644 --- a/src/snapshot/Serialize/Serializer.spec.lua +++ b/src/snapshot/Serialize/Serializer.spec.lua @@ -1,5 +1,5 @@ return function() - local AnonymousFunction = require(script.Parent.AnonymousFunction) + local Markers = require(script.Parent.Markers) local Change = require(script.Parent.Parent.Parent.PropMarkers.Change) local Event = require(script.Parent.Parent.Parent.PropMarkers.Event) local ElementKind = require(script.Parent.Parent.Parent.ElementKind) @@ -51,13 +51,13 @@ return function() end) end) - describe("propKey", function() + describe("tableKey", function() it("should serialize to a named dictionary field", function() local keys = {"foo", "foo1"} for i=1, #keys do local key = keys[i] - local result = Serializer.propKey(key) + local result = Serializer.tableKey(key) expect(result).to.equal(key) end @@ -68,71 +68,103 @@ return function() for i=1, #keys do local key = keys[i] - local result = Serializer.propKey(key) + local result = Serializer.tableKey(key) expect(result).to.equal('["' .. key .. '"]') end end) end) - describe("propValue", function() + describe("tableValue", function() it("should serialize strings", function() - local result = Serializer.propValue("foo") + local result = Serializer.tableValue("foo") expect(result).to.equal('"foo"') end) it("should serialize strings with \"", function() - local result = Serializer.propValue('foo"bar') + local result = Serializer.tableValue('foo"bar') expect(result).to.equal('"foo\\"bar"') end) it("should serialize numbers", function() - local result = Serializer.propValue(10.5) + local result = Serializer.tableValue(10.5) expect(result).to.equal("10.5") end) it("should serialize booleans", function() - expect(Serializer.propValue(true)).to.equal("true") - expect(Serializer.propValue(false)).to.equal("false") + expect(Serializer.tableValue(true)).to.equal("true") + expect(Serializer.tableValue(false)).to.equal("false") end) it("should serialize enum items", function() - local result = Serializer.propValue(Enum.SortOrder.LayoutOrder) + local result = Serializer.tableValue(Enum.SortOrder.LayoutOrder) expect(result).to.equal("Enum.SortOrder.LayoutOrder") end) it("should serialize Color3", function() - local result = Serializer.propValue(Color3.new(0.1, 0.2, 0.3)) + local result = Serializer.tableValue(Color3.new(0.1, 0.2, 0.3)) expect(result).to.equal("Color3.new(0.1, 0.2, 0.3)") end) it("should serialize UDim", function() - local result = Serializer.propValue(UDim.new(1, 0.5)) + local result = Serializer.tableValue(UDim.new(1, 0.5)) expect(result).to.equal("UDim.new(1, 0.5)") end) it("should serialize UDim2", function() - local result = Serializer.propValue(UDim2.new(1, 0.5, 2, 2.5)) + local result = Serializer.tableValue(UDim2.new(1, 0.5, 2, 2.5)) expect(result).to.equal("UDim2.new(1, 0.5, 2, 2.5)") end) it("should serialize Vector2", function() - local result = Serializer.propValue(Vector2.new(1.5, 0.3)) + local result = Serializer.tableValue(Vector2.new(1.5, 0.3)) expect(result).to.equal("Vector2.new(1.5, 0.3)") end) - it("should serialize AnonymousFunction symbol", function() - local result = Serializer.propValue(AnonymousFunction) + it("should serialize markers symbol", function() + for name, marker in pairs(Markers) do + local result = Serializer.tableValue(marker) - expect(result).to.equal("AnonymousFunction") + expect(result).to.equal(("Markers.%s"):format(name)) + end + end) + + it("should serialize Roact.Event events", function() + local result = Serializer.tableValue(Event.Activated) + + expect(result).to.equal("Roact.Event.Activated") + end) + + it("should serialize Roact.Change events", function() + local result = Serializer.tableValue(Change.AbsoluteSize) + + expect(result).to.equal("Roact.Change.AbsoluteSize") + end) + end) + + describe("table", function() + it("should serialize an empty nested table", function() + local output = IndentedOutput.new() + Serializer.table("sub", {}, output) + + expect(output:join()).to.equal("sub = {},") + end) + + it("should serialize an nested table", function() + local output = IndentedOutput.new() + Serializer.table("sub", { + foo = 1, + }, output) + + expect(output:join()).to.equal("sub = {\n foo = 1,\n},") end) end) @@ -156,28 +188,60 @@ return function() it("should serialize Roact.Event", function() local output = IndentedOutput.new() Serializer.props({ - [Event.Activated] = AnonymousFunction, + [Event.Activated] = Markers.AnonymousFunction, }, output) expect(output:join()).to.equal( "props = {\n" - .. " [Roact.Event.Activated] = AnonymousFunction,\n" + .. " [Roact.Event.Activated] = Markers.AnonymousFunction,\n" .. "}," ) end) + it("should sort Roact.Event", function() + local output = IndentedOutput.new() + Serializer.props({ + [Event.Activated] = Markers.AnonymousFunction, + [Event.MouseEnter] = Markers.AnonymousFunction, + }, output) + + expect(output:join()).to.equal( + "props = {\n" + .. " [Roact.Event.Activated] = Markers.AnonymousFunction,\n" + .. " [Roact.Event.MouseEnter] = Markers.AnonymousFunction,\n" + .. "}," + ) + end) + it("should serialize Roact.Change", function() local output = IndentedOutput.new() Serializer.props({ - [Change.Position] = AnonymousFunction, + [Change.Position] = Markers.AnonymousFunction, }, output) expect(output:join()).to.equal( "props = {\n" - .. " [Roact.Change.Position] = AnonymousFunction,\n" + .. " [Roact.Change.Position] = Markers.AnonymousFunction,\n" .. "}," ) end) + + it("should sort props, Roact.Event and Roact.Change", function() + local output = IndentedOutput.new() + Serializer.props({ + foo = 1, + [Event.Activated] = Markers.AnonymousFunction, + [Change.Position] = Markers.AnonymousFunction, + }, output) + + expect(output:join()).to.equal( + "props = {\n" + .. " foo = 1,\n" + .. " [Roact.Event.Activated] = Markers.AnonymousFunction,\n" + .. " [Roact.Change.Position] = Markers.AnonymousFunction,\n" + .. "}," + ) + end) end) describe("children", function() diff --git a/src/snapshot/Serialize/SnapshotData.lua b/src/snapshot/Serialize/SnapshotData.lua index cbbb7f37..6d5ff0d5 100644 --- a/src/snapshot/Serialize/SnapshotData.lua +++ b/src/snapshot/Serialize/SnapshotData.lua @@ -1,6 +1,8 @@ -local AnonymousFunction = require(script.Parent.AnonymousFunction) -local ElementKind = require(script.Parent.Parent.Parent.ElementKind) -local Type = require(script.Parent.Parent.Parent.Type) +local RoactRoot = script.Parent.Parent.Parent +local Markers = require(script.Parent.Markers) +local ElementKind = require(RoactRoot.ElementKind) +local Type = require(RoactRoot.Type) +local Ref = require(RoactRoot.PropMarkers.Ref) local function sortSerializedChildren(childA, childB) return childA.hostKey < childB.hostKey @@ -22,24 +24,41 @@ function SnapshotData.type(wrapperType) return typeData end +function SnapshotData.signal(signal) + local signalToString = tostring(signal) + local signalName = signalToString:match("Signal (%w+)") + + assert(signalName ~= nil, ("Can not extract signal name from %q"):format(signalToString)) + + return { + [Markers.Signal] = signalName + } +end + function SnapshotData.propValue(prop) local propType = type(prop) if propType == "string" or propType == "number" or propType == "boolean" - or propType == "userdata" then return prop - elseif propType == 'function' then - return AnonymousFunction + elseif propType == "function" then + return Markers.AnonymousFunction + + elseif typeof(prop) == "RBXScriptSignal" then + return SnapshotData.signal(prop) + + elseif propType == "userdata" then + return prop else - error(("SnapshotData does not support prop with value %q (type %q)"):format( + warn(("SnapshotData does not support prop with value %q (type %q)"):format( tostring(prop), propType )) + return Markers.Unknown end end @@ -53,6 +72,17 @@ function SnapshotData.props(wrapperProps) then serializedProps[key] = SnapshotData.propValue(prop) + elseif key == Ref then + local current = prop:getValue() + + if current then + serializedProps[key] = { + className = current.ClassName, + } + else + serializedProps[key] = Markers.EmptyRef + end + else error(("SnapshotData does not support prop with key %q (type: %s)"):format( tostring(key), diff --git a/src/snapshot/Serialize/SnapshotData.spec.lua b/src/snapshot/Serialize/SnapshotData.spec.lua index 19d59262..6c1c168c 100644 --- a/src/snapshot/Serialize/SnapshotData.spec.lua +++ b/src/snapshot/Serialize/SnapshotData.spec.lua @@ -1,13 +1,16 @@ return function() local RoactRoot = script.Parent.Parent.Parent - local AnonymousFunction = require(script.Parent.AnonymousFunction) + local Markers = require(script.Parent.Markers) local assertDeepEqual = require(RoactRoot.assertDeepEqual) + local Binding = require(RoactRoot.Binding) local Change = require(RoactRoot.PropMarkers.Change) local Component = require(RoactRoot.Component) local createElement = require(RoactRoot.createElement) + local createRef = require(RoactRoot.createRef) local ElementKind = require(RoactRoot.ElementKind) local Event = require(RoactRoot.PropMarkers.Event) + local Ref = require(RoactRoot.PropMarkers.Ref) local shallow = require(RoactRoot.shallow) local SnapshotData = require(script.Parent.SnapshotData) @@ -72,6 +75,23 @@ return function() end) end) + describe("signal", function() + it("should convert signals", function() + local signalName = "Foo" + local signalMock = setmetatable({}, { + __tostring = function() + return "Signal " .. signalName + end + }) + + local result = SnapshotData.signal(signalMock) + + assertDeepEqual(result, { + [Markers.Signal] = signalName + }) + end) + end) + describe("propValue", function() it("should return the same value", function() local propValues = {7, "hello", Enum.SortOrder.LayoutOrder} @@ -87,7 +107,13 @@ return function() it("should return the AnonymousFunction symbol when given a function", function() local result = SnapshotData.propValue(function() end) - expect(result).to.equal(AnonymousFunction) + expect(result).to.equal(Markers.AnonymousFunction) + end) + + it("should return the Unknown symbol when given an unexpected value", function() + local result = SnapshotData.propValue({}) + + expect(result).to.equal(Markers.Unknown) end) end) @@ -111,7 +137,7 @@ return function() local result = SnapshotData.props(props) assertDeepEqual(result, { - [Event.Activated] = AnonymousFunction, + [Event.Activated] = Markers.AnonymousFunction, }) end) @@ -123,7 +149,37 @@ return function() local result = SnapshotData.props(props) assertDeepEqual(result, { - [Change.Position] = AnonymousFunction, + [Change.Position] = Markers.AnonymousFunction, + }) + end) + + it("should map empty refs to the EmptyRef symbol", function() + local props = { + [Ref] = createRef(), + } + + local result = SnapshotData.props(props) + + assertDeepEqual(result, { + [Ref] = Markers.EmptyRef, + }) + end) + + it("should map refs with value to their symbols", function() + local instanceClassName = "Folder" + local ref = createRef() + Binding.update(ref, Instance.new(instanceClassName)) + + local props = { + [Ref] = ref, + } + + local result = SnapshotData.props(props) + + assertDeepEqual(result, { + [Ref] = { + className = instanceClassName, + }, }) end) @@ -166,7 +222,7 @@ return function() } local expectProps = { LayoutOrder = 3, - [Change.Size] = AnonymousFunction, + [Change.Size] = Markers.AnonymousFunction, } local wrapper = shallow(createElement("Frame", props)) diff --git a/src/snapshot/Snapshot.lua b/src/snapshot/Snapshot.lua index c42b8012..795d6a78 100644 --- a/src/snapshot/Snapshot.lua +++ b/src/snapshot/Snapshot.lua @@ -1,6 +1,6 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") -local AnonymousFunction = require(script.Parent.Serialize.AnonymousFunction) +local Markers = require(script.Parent.Serialize.Markers) local Serialize = require(script.Parent.Serialize) local deepEqual = require(script.Parent.Parent.deepEqual) local ElementKind = require(script.Parent.Parent.ElementKind) @@ -53,6 +53,7 @@ end function Snapshot:serialize() local folder = Snapshot.getSnapshotFolder() + local snapshotSource = Serialize.snapshotDataToString(self.data) local existingData = folder:FindFirstChild(self._identifier) if not existingData then @@ -61,7 +62,7 @@ function Snapshot:serialize() existingData.Parent = folder end - existingData.Value = Serialize.snapshotDataToString(self.data) + existingData.Value = snapshotSource end function Snapshot.getSnapshotFolder() @@ -90,7 +91,7 @@ function Snapshot._loadExistingData(identifier) return loadSnapshot({ Roact = require(script.Parent.Parent), ElementKind = ElementKind, - AnonymousFunction = AnonymousFunction, + Markers = Markers, }) end diff --git a/src/snapshot/init.lua b/src/snapshot/init.lua index d8a568cf..ae253a8d 100644 --- a/src/snapshot/init.lua +++ b/src/snapshot/init.lua @@ -1,7 +1,15 @@ local Serialize = require(script.Serialize) local Snapshot = require(script.Snapshot) +local characterClass = "%w_%-%." +local identifierPattern = "^[" .. characterClass .. "]+$" +local invalidPattern = "[^" .. characterClass .. "]" + return function(identifier, shallowWrapper) + if not identifier:match(identifierPattern) then + error(("Snapshot identifier has invalid character: '%s'"):format(identifier:match(invalidPattern))) + end + local data = Serialize.wrapperToSnapshotData(shallowWrapper) local snapshot = Snapshot.new(identifier, data) From 4183766facfa2360dc0996eb643ef936564d369e Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 24 Jul 2019 17:32:10 -0700 Subject: [PATCH 09/65] Format floats with maximum 7 decimals --- src/snapshot/Serialize/Serializer.lua | 34 ++++++++--- src/snapshot/Serialize/Serializer.spec.lua | 69 +++++++++++++++++----- 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/src/snapshot/Serialize/Serializer.lua b/src/snapshot/Serialize/Serializer.lua index 7f07522f..6e22e44f 100644 --- a/src/snapshot/Serialize/Serializer.lua +++ b/src/snapshot/Serialize/Serializer.lua @@ -42,34 +42,54 @@ function Serializer.tableKey(key) end end +function Serializer.number(value) + local _, fraction = math.modf(value) + + if fraction == 0 then + return ("%s"):format(tostring(value)) + else + return ("%0.7f"):format(value):gsub("%.?0+$", "") + end +end + function Serializer.tableValue(value) local valueType = typeof(value) if valueType == "string" then return ("%q"):format(value) - elseif valueType == "number" or valueType == "boolean" then + elseif valueType == "number" then + return Serializer.number(value) + + elseif valueType == "boolean" then return ("%s"):format(tostring(value)) elseif valueType == "Color3" then - return ("Color3.new(%s, %s, %s)"):format(value.r, value.g, value.b) + return ("Color3.new(%s, %s, %s)"):format( + Serializer.number(value.r), + Serializer.number(value.g), + Serializer.number(value.b) + ) elseif valueType == "EnumItem" then return ("%s"):format(tostring(value)) elseif valueType == "UDim" then - return ("UDim.new(%s, %s)"):format(value.Scale, value.Offset) + return ("UDim.new(%s, %d)"):format(Serializer.number(value.Scale), value.Offset) elseif valueType == "UDim2" then - return ("UDim2.new(%s, %s, %s, %s)"):format( - value.X.Scale, + return ("UDim2.new(%s, %d, %s, %d)"):format( + Serializer.number(value.X.Scale), value.X.Offset, - value.Y.Scale, + Serializer.number(value.Y.Scale), value.Y.Offset ) elseif valueType == "Vector2" then - return ("Vector2.new(%s, %s)"):format(value.X, value.Y) + return ("Vector2.new(%s, %s)"):format( + Serializer.number(value.X), + Serializer.number(value.Y) + ) elseif Type.of(value) == Type.HostEvent then return ("Roact.Event.%s"):format(value.name) diff --git a/src/snapshot/Serialize/Serializer.spec.lua b/src/snapshot/Serialize/Serializer.spec.lua index 91f69a0b..786ab5a7 100644 --- a/src/snapshot/Serialize/Serializer.spec.lua +++ b/src/snapshot/Serialize/Serializer.spec.lua @@ -1,9 +1,12 @@ return function() + local RoactRoot = script.Parent.Parent.Parent + local Markers = require(script.Parent.Markers) - local Change = require(script.Parent.Parent.Parent.PropMarkers.Change) - local Event = require(script.Parent.Parent.Parent.PropMarkers.Event) - local ElementKind = require(script.Parent.Parent.Parent.ElementKind) + local Change = require(RoactRoot.PropMarkers.Change) + local Event = require(RoactRoot.PropMarkers.Event) + local ElementKind = require(RoactRoot.ElementKind) local IndentedOutput = require(script.Parent.IndentedOutput) + local Ref = require(RoactRoot.PropMarkers.Ref) local Serializer = require(script.Parent.Serializer) describe("type", function() @@ -75,6 +78,25 @@ return function() end) end) + describe("number", function() + it("should format integers", function() + expect(Serializer.number(1)).to.equal("1") + expect(Serializer.number(0)).to.equal("0") + expect(Serializer.number(10)).to.equal("10") + end) + + it("should minimize floating points zeros", function() + expect(Serializer.number(1.2)).to.equal("1.2") + expect(Serializer.number(0.002)).to.equal("0.002") + expect(Serializer.number(5.5001)).to.equal("5.5001") + end) + + it("should keep only 7 decimals", function() + expect(Serializer.number(0.123456789)).to.equal("0.1234568") + expect(Serializer.number(0.123456709)).to.equal("0.1234567") + end) + end) + describe("tableValue", function() it("should serialize strings", function() local result = Serializer.tableValue("foo") @@ -112,15 +134,15 @@ return function() end) it("should serialize UDim", function() - local result = Serializer.tableValue(UDim.new(1, 0.5)) + local result = Serializer.tableValue(UDim.new(1.2, 0)) - expect(result).to.equal("UDim.new(1, 0.5)") + expect(result).to.equal("UDim.new(1.2, 0)") end) it("should serialize UDim2", function() - local result = Serializer.tableValue(UDim2.new(1, 0.5, 2, 2.5)) + local result = Serializer.tableValue(UDim2.new(1.5, 5, 2, 3)) - expect(result).to.equal("UDim2.new(1, 0.5, 2, 2.5)") + expect(result).to.equal("UDim2.new(1.5, 5, 2, 3)") end) it("should serialize Vector2", function() @@ -207,9 +229,9 @@ return function() expect(output:join()).to.equal( "props = {\n" - .. " [Roact.Event.Activated] = Markers.AnonymousFunction,\n" - .. " [Roact.Event.MouseEnter] = Markers.AnonymousFunction,\n" - .. "}," + .. " [Roact.Event.Activated] = Markers.AnonymousFunction,\n" + .. " [Roact.Event.MouseEnter] = Markers.AnonymousFunction,\n" + .. "}," ) end) @@ -226,20 +248,37 @@ return function() ) end) - it("should sort props, Roact.Event and Roact.Change", function() + it("should sort props, Roact.Event, Roact.Change and Ref", function() local output = IndentedOutput.new() Serializer.props({ foo = 1, [Event.Activated] = Markers.AnonymousFunction, [Change.Position] = Markers.AnonymousFunction, + [Ref] = Markers.EmptyRef, + }, output) + + expect(output:join()).to.equal( + "props = {\n" + .. " foo = 1,\n" + .. " [Roact.Event.Activated] = Markers.AnonymousFunction,\n" + .. " [Roact.Change.Position] = Markers.AnonymousFunction,\n" + .. " [Roact.Ref] = Markers.EmptyRef,\n" + .. "}," + ) + end) + + it("should sort props within themselves", function() + local output = IndentedOutput.new() + Serializer.props({ + foo = 1, + bar = 2, }, output) expect(output:join()).to.equal( "props = {\n" - .. " foo = 1,\n" - .. " [Roact.Event.Activated] = Markers.AnonymousFunction,\n" - .. " [Roact.Change.Position] = Markers.AnonymousFunction,\n" - .. "}," + .. " bar = 2,\n" + .. " foo = 1,\n" + .. "}," ) end) end) From 830c0b617bbc61a6541acf08e85f3f7de25a9289 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 24 Jul 2019 17:41:31 -0700 Subject: [PATCH 10/65] Remove Roact.shallow for virtualTree method --- src/{shallow.lua => ShallowWrapper.lua} | 27 +++------- ...allow.spec.lua => ShallowWrapper.spec.lua} | 17 +++++- src/VirtualTree.lua | 33 ++++++++++++ src/VirtualTree.spec.lua | 24 +++++++++ src/createReconciler.lua | 15 ++---- src/init.lua | 3 -- src/init.spec.lua | 1 - src/snapshot.spec.lua | 54 +++++++++++++++++++ src/snapshot/Serialize/SnapshotData.spec.lua | 17 +++++- src/snapshot/Snapshot.lua | 3 ++ src/snapshot/Snapshot.spec.lua | 17 ++++-- 11 files changed, 168 insertions(+), 43 deletions(-) rename src/{shallow.lua => ShallowWrapper.lua} (88%) rename src/{shallow.spec.lua => ShallowWrapper.spec.lua} (97%) create mode 100644 src/VirtualTree.lua create mode 100644 src/VirtualTree.spec.lua create mode 100644 src/snapshot.spec.lua diff --git a/src/shallow.lua b/src/ShallowWrapper.lua similarity index 88% rename from src/shallow.lua rename to src/ShallowWrapper.lua index 01ef95d4..6f768c83 100644 --- a/src/shallow.lua +++ b/src/ShallowWrapper.lua @@ -1,12 +1,8 @@ -local createReconciler = require(script.Parent.createReconciler) local Children = require(script.Parent.PropMarkers.Children) -local RobloxRenderer = require(script.Parent.RobloxRenderer) local ElementKind = require(script.Parent.ElementKind) local ElementUtils = require(script.Parent.ElementUtils) local snapshot = require(script.Parent.snapshot) -local robloxReconciler = createReconciler(RobloxRenderer) - local ShallowWrapper = {} local ShallowWrapperMetatable = { __index = ShallowWrapper, @@ -38,16 +34,16 @@ end local function findNextVirtualNode(virtualNode, maxDepth) local currentDepth = 0 - local wrapVirtualNode = virtualNode - local nextNode = wrapVirtualNode.children[ElementUtils.UseParentKey] + local currentNode = virtualNode + local nextNode = currentNode.children[ElementUtils.UseParentKey] while currentDepth < maxDepth and nextNode ~= nil do - wrapVirtualNode = nextNode - nextNode = wrapVirtualNode.children[ElementUtils.UseParentKey] + currentNode = nextNode + nextNode = currentNode.children[ElementUtils.UseParentKey] currentDepth = currentDepth + 1 end - return wrapVirtualNode + return currentNode end local ContraintFunctions = { @@ -229,15 +225,4 @@ function ShallowWrapper:_satisfiesAllContraints(constraints) return true end -local function shallow(element, options) - options = options or {} - - local tempParent = Instance.new("Folder") - local virtualNode = robloxReconciler.mountVirtualNode(element, tempParent, "ShallowTree") - - local maxDepth = options.depth or 1 - - return ShallowWrapper.new(virtualNode, maxDepth) -end - -return shallow \ No newline at end of file +return ShallowWrapper \ No newline at end of file diff --git a/src/shallow.spec.lua b/src/ShallowWrapper.spec.lua similarity index 97% rename from src/shallow.spec.lua rename to src/ShallowWrapper.spec.lua index 256ed98a..20799734 100644 --- a/src/shallow.spec.lua +++ b/src/ShallowWrapper.spec.lua @@ -1,12 +1,27 @@ return function() - local shallow = require(script.Parent.shallow) + local ShallowWrapper = require(script.Parent.ShallowWrapper) local assertDeepEqual = require(script.Parent.assertDeepEqual) local Children = require(script.Parent.PropMarkers.Children) local ElementKind = require(script.Parent.ElementKind) local createElement = require(script.Parent.createElement) local createFragment = require(script.Parent.createFragment) + local createReconciler = require(script.Parent.createReconciler) local RoactComponent = require(script.Parent.Component) + local RobloxRenderer = require(script.Parent.RobloxRenderer) + + local robloxReconciler = createReconciler(RobloxRenderer) + + local function shallow(element, options) + options = options or {} + local maxDepth = options.depth or 1 + local hostKey = options.hostKey or "ShallowTree" + local hostParent = options.hostParent or Instance.new("Folder") + + local virtualNode = robloxReconciler.mountVirtualNode(element, hostParent, hostKey) + + return ShallowWrapper.new(virtualNode, maxDepth) + end describe("single host element", function() local className = "TextLabel" diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua new file mode 100644 index 00000000..451d50e5 --- /dev/null +++ b/src/VirtualTree.lua @@ -0,0 +1,33 @@ +local ShallowWrapper = require(script.Parent.ShallowWrapper) +local Type = require(script.Parent.Type) + +local config = require(script.Parent.GlobalConfig).get() + +local VirtualTree = {} + +function VirtualTree.new(rootNode, internalDataSymbol) + local tree = { + [Type] = Type.VirtualTree, + [internalDataSymbol] = { + rootNode = rootNode, + mounted = true, + } + } + + function tree:getTestRenderOutput(options) + options = options or {} + local maxDepth = options.depth or 1 + local internalData = self[internalDataSymbol] + + if config.typeChecks then + assert(Type.of(self) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(internalData.mounted, "Cannot get render output from an unmounted Roact tree") + end + + return ShallowWrapper.new(internalData.rootNode, maxDepth) + end + + return tree +end + +return VirtualTree \ No newline at end of file diff --git a/src/VirtualTree.spec.lua b/src/VirtualTree.spec.lua new file mode 100644 index 00000000..764a4ce3 --- /dev/null +++ b/src/VirtualTree.spec.lua @@ -0,0 +1,24 @@ +return function() + local createElement = require(script.Parent.createElement) + local createReconciler = require(script.Parent.createReconciler) + local RobloxRenderer = require(script.Parent.RobloxRenderer) + + local robloxReconciler = createReconciler(RobloxRenderer) + + describe("getTestRenderOutput", function() + it("should return a ShallowWrapper with the given depth", function() + local function Component() + return createElement("Frame") + end + local element = createElement(Component) + + local tree = robloxReconciler.mountVirtualTree(element) + + local wrapper = tree:getTestRenderOutput({ + depth = 0, + }) + + expect(wrapper.type.functionComponent).to.equal(Component) + end) + end) +end \ No newline at end of file diff --git a/src/createReconciler.lua b/src/createReconciler.lua index bcab1ba2..c5ff948c 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -4,6 +4,7 @@ local ElementUtils = require(script.Parent.ElementUtils) local Children = require(script.Parent.PropMarkers.Children) local Symbol = require(script.Parent.Symbol) local internalAssert = require(script.Parent.internalAssert) +local VirtualTree = require(script.Parent.VirtualTree) local config = require(script.Parent.GlobalConfig).get() @@ -359,19 +360,9 @@ local function createReconciler(renderer) hostKey = "RoactTree" end - local tree = { - [Type] = Type.VirtualTree, - [InternalData] = { - -- The root node of the tree, which starts into the hierarchy of - -- Roact component instances. - rootNode = nil, - mounted = true, - }, - } - - tree[InternalData].rootNode = mountVirtualNode(element, hostParent, hostKey) + local rootNode = mountVirtualNode(element, hostParent, hostKey) - return tree + return VirtualTree.new(rootNode, InternalData) end --[[ diff --git a/src/init.lua b/src/init.lua index 2e33cc5d..f002f975 100644 --- a/src/init.lua +++ b/src/init.lua @@ -6,7 +6,6 @@ local GlobalConfig = require(script.GlobalConfig) local createReconciler = require(script.createReconciler) local createReconcilerCompat = require(script.createReconcilerCompat) local RobloxRenderer = require(script.RobloxRenderer) -local shallow = require(script.shallow) local strict = require(script.strict) local Binding = require(script.Binding) @@ -40,8 +39,6 @@ local Roact = strict { setGlobalConfig = GlobalConfig.set, - shallow = shallow, - -- APIs that may change in the future without warning UNSTABLE = { }, diff --git a/src/init.spec.lua b/src/init.spec.lua index 390d3846..7fcf79c8 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -13,7 +13,6 @@ return function() update = "function", oneChild = "function", setGlobalConfig = "function", - shallow = "function", -- These functions are deprecated and throw warnings! reify = "function", diff --git a/src/snapshot.spec.lua b/src/snapshot.spec.lua new file mode 100644 index 00000000..fedd305f --- /dev/null +++ b/src/snapshot.spec.lua @@ -0,0 +1,54 @@ +return function() + local Change = require(script.Parent.PropMarkers.Change) + local createElement = require(script.Parent.createElement) + local createReconciler = require(script.Parent.createReconciler) + local RobloxRenderer = require(script.Parent.RobloxRenderer) + local snapshot = require(script.Parent.snapshot) + + local robloxReconciler = createReconciler(RobloxRenderer) + + it("should match previous snapshot format of host component", function() + local element = createElement("Frame", { + BackgroundTransparency = 0.205, + Visible = true, + [Change.AbsoluteSize] = function() end, + }) + + local tree = robloxReconciler.mountVirtualTree(element) + local wrapper = tree:getTestRenderOutput() + + snapshot("host-frame-props", wrapper):match() + end) + + it("should match previous snapshot format of function component", function() + local function ChildComponent(props) + return createElement("TextLabel", props) + end + + local element = createElement("Frame", {}, { + LabelA = createElement(ChildComponent, { + Text = "I am label A" + }), + LabelB = createElement(ChildComponent, { + Text = "I am label B" + }), + }) + + local tree = robloxReconciler.mountVirtualTree(element) + local wrapper = tree:getTestRenderOutput() + + snapshot("function-component", wrapper):match() + end) + + it("should throw if the identifier contains invalid characters", function() + local invalidCharacters = {"\\", "/", "?"} + + for i=1, #invalidCharacters do + local function shouldThrow() + snapshot("id" .. invalidCharacters[i], {}) + end + + expect(shouldThrow).to.throw() + end + end) +end \ No newline at end of file diff --git a/src/snapshot/Serialize/SnapshotData.spec.lua b/src/snapshot/Serialize/SnapshotData.spec.lua index 6c1c168c..f19c6809 100644 --- a/src/snapshot/Serialize/SnapshotData.spec.lua +++ b/src/snapshot/Serialize/SnapshotData.spec.lua @@ -7,14 +7,29 @@ return function() local Change = require(RoactRoot.PropMarkers.Change) local Component = require(RoactRoot.Component) local createElement = require(RoactRoot.createElement) + local createReconciler = require(RoactRoot.createReconciler) local createRef = require(RoactRoot.createRef) local ElementKind = require(RoactRoot.ElementKind) local Event = require(RoactRoot.PropMarkers.Event) local Ref = require(RoactRoot.PropMarkers.Ref) - local shallow = require(RoactRoot.shallow) + local RobloxRenderer = require(RoactRoot.RobloxRenderer) + local ShallowWrapper = require(RoactRoot.ShallowWrapper) local SnapshotData = require(script.Parent.SnapshotData) + local robloxReconciler = createReconciler(RobloxRenderer) + + local function shallow(element, options) + options = options or {} + local maxDepth = options.depth or 1 + local hostKey = options.hostKey or "ShallowTree" + local hostParent = options.hostParent or Instance.new("Folder") + + local virtualNode = robloxReconciler.mountVirtualNode(element, hostParent, hostKey) + + return ShallowWrapper.new(virtualNode, maxDepth) + end + describe("type", function() describe("host elements", function() it("should contain the host kind", function() diff --git a/src/snapshot/Snapshot.lua b/src/snapshot/Snapshot.lua index 795d6a78..080afc22 100644 --- a/src/snapshot/Snapshot.lua +++ b/src/snapshot/Snapshot.lua @@ -41,6 +41,9 @@ function Snapshot:match() return end + local failingSnapshot = Snapshot.new(self._identifier .. ".FAILED", self.data) + failingSnapshot:serialize() + local innerMessage = innerMessageTemplate :gsub("{1}", "new") :gsub("{2}", "existing") diff --git a/src/snapshot/Snapshot.spec.lua b/src/snapshot/Snapshot.spec.lua index 2453f827..922cd82f 100644 --- a/src/snapshot/Snapshot.spec.lua +++ b/src/snapshot/Snapshot.spec.lua @@ -6,7 +6,8 @@ return function() local snapshotFolder = Instance.new("Folder") local originalGetSnapshotFolder = Snapshot.getSnapshotFolder - Snapshot.getSnapshotFolder = function() + + local function mockGetSnapshotFolder() return snapshotFolder end @@ -92,6 +93,8 @@ return function() describe("serialize", function() it("should create a StringValue if it does not exist", function() + Snapshot.getSnapshotFolder = mockGetSnapshotFolder + local identifier = "foo" local snapshot = Snapshot.new(identifier, { @@ -106,27 +109,33 @@ return function() snapshot:serialize() local stringValue = snapshotFolder:FindFirstChild(identifier) + Snapshot.getSnapshotFolder = originalGetSnapshotFolder + expect(stringValue).to.be.ok() expect(stringValue.Value:len() > 0).to.equal(true) + + stringValue:Destroy() end) end) describe("_loadExistingData", function() it("should return nil if data is not found", function() + Snapshot.getSnapshotFolder = mockGetSnapshotFolder + local result = Snapshot._loadExistingData("foo") + Snapshot.getSnapshotFolder = originalGetSnapshotFolder + expect(result).never.to.be.ok() end) end) describe("getSnapshotFolder", function() it("should create a folder in the ReplicatedStorage if it is not found", function() - local folder = originalGetSnapshotFolder() + local folder = Snapshot.getSnapshotFolder() expect(folder).to.be.ok() expect(folder.Parent).to.equal(game:GetService("ReplicatedStorage")) - - folder:Destroy() end) end) end \ No newline at end of file From 39eae67f7322fcc3756663ad690d3b00c2d5d58c Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 25 Jul 2019 16:37:01 -0700 Subject: [PATCH 11/65] Rename failed snapshot to new snapshot --- src/snapshot/Snapshot.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snapshot/Snapshot.lua b/src/snapshot/Snapshot.lua index 080afc22..0c49a684 100644 --- a/src/snapshot/Snapshot.lua +++ b/src/snapshot/Snapshot.lua @@ -41,8 +41,8 @@ function Snapshot:match() return end - local failingSnapshot = Snapshot.new(self._identifier .. ".FAILED", self.data) - failingSnapshot:serialize() + local newSnapshot = Snapshot.new(self._identifier .. ".NEW", self.data) + newSnapshot:serialize() local innerMessage = innerMessageTemplate :gsub("{1}", "new") From 16e1ba01e41ca3615cc5e9ab6ab56b3b20382454 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 25 Jul 2019 17:08:23 -0700 Subject: [PATCH 12/65] Update snapshot tests --- src/snapshot.spec.lua | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/snapshot.spec.lua b/src/snapshot.spec.lua index fedd305f..bad525e5 100644 --- a/src/snapshot.spec.lua +++ b/src/snapshot.spec.lua @@ -7,29 +7,32 @@ return function() local robloxReconciler = createReconciler(RobloxRenderer) - it("should match previous snapshot format of host component", function() + it("should match snapshot of host component with multiple props", function() local element = createElement("Frame", { + BackgroundColor3 = Color3.new(0.1, 0.2, 0.3), BackgroundTransparency = 0.205, + ClipsDescendants = false, + SizeConstraint = Enum.SizeConstraint.RelativeXY, Visible = true, - [Change.AbsoluteSize] = function() end, + ZIndex = 5, }) local tree = robloxReconciler.mountVirtualTree(element) local wrapper = tree:getTestRenderOutput() - snapshot("host-frame-props", wrapper):match() + snapshot("host-frame-with-multiple-props", wrapper):match() end) - it("should match previous snapshot format of function component", function() - local function ChildComponent(props) + it("should match snapshot of function component children", function() + local function LabelComponent(props) return createElement("TextLabel", props) end local element = createElement("Frame", {}, { - LabelA = createElement(ChildComponent, { + LabelA = createElement(LabelComponent, { Text = "I am label A" }), - LabelB = createElement(ChildComponent, { + LabelB = createElement(LabelComponent, { Text = "I am label B" }), }) @@ -37,7 +40,7 @@ return function() local tree = robloxReconciler.mountVirtualTree(element) local wrapper = tree:getTestRenderOutput() - snapshot("function-component", wrapper):match() + snapshot("function-component-children", wrapper):match() end) it("should throw if the identifier contains invalid characters", function() From c81d22fef5c1f37980f5ea74b2b562ede89780a2 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 25 Jul 2019 17:24:23 -0700 Subject: [PATCH 13/65] Add snapshot tests --- src/snapshot.spec.lua | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/snapshot.spec.lua b/src/snapshot.spec.lua index bad525e5..db4f3840 100644 --- a/src/snapshot.spec.lua +++ b/src/snapshot.spec.lua @@ -1,7 +1,9 @@ return function() local Change = require(script.Parent.PropMarkers.Change) + local Component = require(script.Parent.Component) local createElement = require(script.Parent.createElement) local createReconciler = require(script.Parent.createReconciler) + local Event = require(script.Parent.PropMarkers.Event) local RobloxRenderer = require(script.Parent.RobloxRenderer) local snapshot = require(script.Parent.snapshot) @@ -43,6 +45,44 @@ return function() snapshot("function-component-children", wrapper):match() end) + it("should match snapshot of stateful component", function() + local StatefulComponent = Component:extend("CoolComponent") + + function StatefulComponent:render() + return createElement("TextLabel") + end + + local element = createElement("Frame", {}, { + Child = createElement(StatefulComponent, { + label = { + Text = "foo", + }, + }), + }) + + local tree = robloxReconciler.mountVirtualTree(element) + local wrapper = tree:getTestRenderOutput() + + snapshot("stateful-component-children", wrapper):match() + end) + + it("should match snapshot with event props", function() + local function emptyFunction() + end + + local element = createElement("Frame", { + [Change.AbsoluteSize] = emptyFunction, + [Change.Visible] = emptyFunction, + [Event.MouseEnter] = emptyFunction, + [Event.MouseLeave] = emptyFunction, + }) + + local tree = robloxReconciler.mountVirtualTree(element) + local wrapper = tree:getTestRenderOutput() + + snapshot("component-with-event-props", wrapper):match() + end) + it("should throw if the identifier contains invalid characters", function() local invalidCharacters = {"\\", "/", "?"} From 9b09be8039e42be7237a9d7f0b0ced77c285441a Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 25 Jul 2019 17:31:38 -0700 Subject: [PATCH 14/65] Update snapshot tests --- src/snapshot.spec.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/snapshot.spec.lua b/src/snapshot.spec.lua index db4f3840..ece942f2 100644 --- a/src/snapshot.spec.lua +++ b/src/snapshot.spec.lua @@ -11,9 +11,11 @@ return function() it("should match snapshot of host component with multiple props", function() local element = createElement("Frame", { + AnchorPoint = Vector2.new(0, 0.5), BackgroundColor3 = Color3.new(0.1, 0.2, 0.3), BackgroundTransparency = 0.205, ClipsDescendants = false, + Size = UDim2.new(0.5, 0, 0.4, 1), SizeConstraint = Enum.SizeConstraint.RelativeXY, Visible = true, ZIndex = 5, @@ -32,10 +34,10 @@ return function() local element = createElement("Frame", {}, { LabelA = createElement(LabelComponent, { - Text = "I am label A" + Text = "I am label A", }), LabelB = createElement(LabelComponent, { - Text = "I am label B" + Text = "I am label B", }), }) From 5944bf3e727fc0e44e9ebc0904f2f8dd763c3b28 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 30 Jul 2019 14:40:30 -0700 Subject: [PATCH 15/65] Remove missing lemur events --- src/snapshot.spec.lua | 6 +++--- src/snapshot/Snapshot.lua | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/snapshot.spec.lua b/src/snapshot.spec.lua index ece942f2..b793363d 100644 --- a/src/snapshot.spec.lua +++ b/src/snapshot.spec.lua @@ -72,11 +72,11 @@ return function() local function emptyFunction() end - local element = createElement("Frame", { + local element = createElement("TextButton", { [Change.AbsoluteSize] = emptyFunction, [Change.Visible] = emptyFunction, - [Event.MouseEnter] = emptyFunction, - [Event.MouseLeave] = emptyFunction, + [Event.Activated] = emptyFunction, + [Event.MouseButton1Click] = emptyFunction, }) local tree = robloxReconciler.mountVirtualTree(element) diff --git a/src/snapshot/Snapshot.lua b/src/snapshot/Snapshot.lua index 0c49a684..75a71587 100644 --- a/src/snapshot/Snapshot.lua +++ b/src/snapshot/Snapshot.lua @@ -59,7 +59,7 @@ function Snapshot:serialize() local snapshotSource = Serialize.snapshotDataToString(self.data) local existingData = folder:FindFirstChild(self._identifier) - if not existingData then + if not (existingData and existingData:IsA('StringValue')) then existingData = Instance.new("StringValue") existingData.Name = self._identifier existingData.Parent = folder From ae299a523932700bff2326ac0b5fc960d0636bef Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 5 Aug 2019 11:13:33 -0700 Subject: [PATCH 16/65] Pass virtual mounted state in constructor --- src/VirtualTree.lua | 4 ++-- src/createReconciler.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua index 451d50e5..8e7c7aa2 100644 --- a/src/VirtualTree.lua +++ b/src/VirtualTree.lua @@ -5,12 +5,12 @@ local config = require(script.Parent.GlobalConfig).get() local VirtualTree = {} -function VirtualTree.new(rootNode, internalDataSymbol) +function VirtualTree.new(rootNode, internalDataSymbol, mounted) local tree = { [Type] = Type.VirtualTree, [internalDataSymbol] = { rootNode = rootNode, - mounted = true, + mounted = mounted, } } diff --git a/src/createReconciler.lua b/src/createReconciler.lua index c5ff948c..c207b911 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -362,7 +362,7 @@ local function createReconciler(renderer) local rootNode = mountVirtualNode(element, hostParent, hostKey) - return VirtualTree.new(rootNode, InternalData) + return VirtualTree.new(rootNode, InternalData, true) end --[[ From 60b18f29319b8e89f711baeb7ffd7997680b6336 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 5 Aug 2019 13:22:05 -0700 Subject: [PATCH 17/65] Rename Snapshot to SnapshotMatcher --- .../{Snapshot.lua => SnapshotMatcher.lua} | 24 ++++---- ...shot.spec.lua => SnapshotMatcher.spec.lua} | 55 +++++++++---------- src/snapshot/init.lua | 6 +- 3 files changed, 42 insertions(+), 43 deletions(-) rename src/snapshot/{Snapshot.lua => SnapshotMatcher.lua} (78%) rename src/snapshot/{Snapshot.spec.lua => SnapshotMatcher.spec.lua} (62%) diff --git a/src/snapshot/Snapshot.lua b/src/snapshot/SnapshotMatcher.lua similarity index 78% rename from src/snapshot/Snapshot.lua rename to src/snapshot/SnapshotMatcher.lua index 75a71587..93c4ed2e 100644 --- a/src/snapshot/Snapshot.lua +++ b/src/snapshot/SnapshotMatcher.lua @@ -8,19 +8,19 @@ local ElementKind = require(script.Parent.Parent.ElementKind) local SnapshotFolderName = "RoactSnapshots" local SnapshotFolder = ReplicatedStorage:FindFirstChild(SnapshotFolderName) -local Snapshot = {} +local SnapshotMatcher = {} local SnapshotMetatable = { - __index = Snapshot, + __index = SnapshotMatcher, __tostring = function(snapshot) return Serialize.snapshotDataToString(snapshot.data) end } -function Snapshot.new(identifier, data) +function SnapshotMatcher.new(identifier, data) local snapshot = { _identifier = identifier, data = data, - _existingData = Snapshot._loadExistingData(identifier), + _existingData = SnapshotMatcher._loadExistingData(identifier), } setmetatable(snapshot, SnapshotMetatable) @@ -28,7 +28,7 @@ function Snapshot.new(identifier, data) return snapshot end -function Snapshot:match() +function SnapshotMatcher:match() if self._existingData == nil then self:serialize() self._existingData = self.data @@ -41,7 +41,7 @@ function Snapshot:match() return end - local newSnapshot = Snapshot.new(self._identifier .. ".NEW", self.data) + local newSnapshot = SnapshotMatcher.new(self._identifier .. ".NEW", self.data) newSnapshot:serialize() local innerMessage = innerMessageTemplate @@ -53,8 +53,8 @@ function Snapshot:match() error(message, 2) end -function Snapshot:serialize() - local folder = Snapshot.getSnapshotFolder() +function SnapshotMatcher:serialize() + local folder = SnapshotMatcher.getSnapshotFolder() local snapshotSource = Serialize.snapshotDataToString(self.data) local existingData = folder:FindFirstChild(self._identifier) @@ -68,7 +68,7 @@ function Snapshot:serialize() existingData.Value = snapshotSource end -function Snapshot.getSnapshotFolder() +function SnapshotMatcher.getSnapshotFolder() SnapshotFolder = ReplicatedStorage:FindFirstChild(SnapshotFolderName) if not SnapshotFolder then @@ -80,8 +80,8 @@ function Snapshot.getSnapshotFolder() return SnapshotFolder end -function Snapshot._loadExistingData(identifier) - local folder = Snapshot.getSnapshotFolder() +function SnapshotMatcher._loadExistingData(identifier) + local folder = SnapshotMatcher.getSnapshotFolder() local existingData = folder:FindFirstChild(identifier) @@ -98,4 +98,4 @@ function Snapshot._loadExistingData(identifier) }) end -return Snapshot \ No newline at end of file +return SnapshotMatcher \ No newline at end of file diff --git a/src/snapshot/Snapshot.spec.lua b/src/snapshot/SnapshotMatcher.spec.lua similarity index 62% rename from src/snapshot/Snapshot.spec.lua rename to src/snapshot/SnapshotMatcher.spec.lua index 922cd82f..ec7a2042 100644 --- a/src/snapshot/Snapshot.spec.lua +++ b/src/snapshot/SnapshotMatcher.spec.lua @@ -1,17 +1,17 @@ return function() - local Snapshot = require(script.Parent.Snapshot) + local SnapshotMatcher = require(script.Parent.SnapshotMatcher) local ElementKind = require(script.Parent.Parent.ElementKind) local createSpy = require(script.Parent.Parent.createSpy) local snapshotFolder = Instance.new("Folder") - local originalGetSnapshotFolder = Snapshot.getSnapshotFolder + local originalGetSnapshotFolder = SnapshotMatcher.getSnapshotFolder local function mockGetSnapshotFolder() return snapshotFolder end - local originalLoadExistingData = Snapshot._loadExistingData + local originalLoadExistingData = SnapshotMatcher._loadExistingData local loadExistingDataSpy = nil describe("match", function() @@ -23,44 +23,43 @@ return function() loadExistingDataSpy = createSpy(function(identifier) return snapshotMap[identifier] end) - Snapshot._loadExistingData = loadExistingDataSpy.value + SnapshotMatcher._loadExistingData = loadExistingDataSpy.value end local function cleanTest() loadExistingDataSpy = nil - Snapshot._loadExistingData = originalLoadExistingData + SnapshotMatcher._loadExistingData = originalLoadExistingData end it("should serialize the snapshot if no data is found", function() beforeTest() - local data = {} - + local snapshot = {} local serializeSpy = createSpy() - local snapshot = Snapshot.new("foo", data) - snapshot.serialize = serializeSpy.value + local matcher = SnapshotMatcher.new("foo", snapshot) + matcher.serialize = serializeSpy.value - snapshot:match() + matcher:match() cleanTest() - serializeSpy:assertCalledWith(snapshot) + serializeSpy:assertCalledWith(matcher) end) it("should not serialize if the snapshot already exist", function() beforeTest() - local data = {} + local snapshot = {} local identifier = "foo" - snapshotMap[identifier] = data + snapshotMap[identifier] = snapshot local serializeSpy = createSpy() - local snapshot = Snapshot.new(identifier, data) - snapshot.serialize = serializeSpy.value + local matcher = SnapshotMatcher.new(identifier, snapshot) + matcher.serialize = serializeSpy.value - snapshot:match() + matcher:match() cleanTest() @@ -70,7 +69,7 @@ return function() it("should throw an error if the previous snapshot does not match", function() beforeTest() - local data = {} + local snapshot = {} local identifier = "foo" snapshotMap[identifier] = { Key = "Value" @@ -78,11 +77,11 @@ return function() local serializeSpy = createSpy() - local snapshot = Snapshot.new(identifier, data) - snapshot.serialize = serializeSpy.value + local matcher = SnapshotMatcher.new(identifier, snapshot) + matcher.serialize = serializeSpy.value local function shouldThrow() - snapshot:match() + matcher:match() end cleanTest() @@ -93,11 +92,11 @@ return function() describe("serialize", function() it("should create a StringValue if it does not exist", function() - Snapshot.getSnapshotFolder = mockGetSnapshotFolder + SnapshotMatcher.getSnapshotFolder = mockGetSnapshotFolder local identifier = "foo" - local snapshot = Snapshot.new(identifier, { + local matcher = SnapshotMatcher.new(identifier, { type = { kind = ElementKind.Function, }, @@ -106,10 +105,10 @@ return function() children = {}, }) - snapshot:serialize() + matcher:serialize() local stringValue = snapshotFolder:FindFirstChild(identifier) - Snapshot.getSnapshotFolder = originalGetSnapshotFolder + SnapshotMatcher.getSnapshotFolder = originalGetSnapshotFolder expect(stringValue).to.be.ok() expect(stringValue.Value:len() > 0).to.equal(true) @@ -120,11 +119,11 @@ return function() describe("_loadExistingData", function() it("should return nil if data is not found", function() - Snapshot.getSnapshotFolder = mockGetSnapshotFolder + SnapshotMatcher.getSnapshotFolder = mockGetSnapshotFolder - local result = Snapshot._loadExistingData("foo") + local result = SnapshotMatcher._loadExistingData("foo") - Snapshot.getSnapshotFolder = originalGetSnapshotFolder + SnapshotMatcher.getSnapshotFolder = originalGetSnapshotFolder expect(result).never.to.be.ok() end) @@ -132,7 +131,7 @@ return function() describe("getSnapshotFolder", function() it("should create a folder in the ReplicatedStorage if it is not found", function() - local folder = Snapshot.getSnapshotFolder() + local folder = SnapshotMatcher.getSnapshotFolder() expect(folder).to.be.ok() expect(folder.Parent).to.equal(game:GetService("ReplicatedStorage")) diff --git a/src/snapshot/init.lua b/src/snapshot/init.lua index ae253a8d..fcfe9caa 100644 --- a/src/snapshot/init.lua +++ b/src/snapshot/init.lua @@ -1,5 +1,5 @@ local Serialize = require(script.Serialize) -local Snapshot = require(script.Snapshot) +local SnapshotMatcher = require(script.SnapshotMatcher) local characterClass = "%w_%-%." local identifierPattern = "^[" .. characterClass .. "]+$" @@ -11,7 +11,7 @@ return function(identifier, shallowWrapper) end local data = Serialize.wrapperToSnapshotData(shallowWrapper) - local snapshot = Snapshot.new(identifier, data) + local matcher = SnapshotMatcher.new(identifier, data) - return snapshot + return matcher end From 2773da60a364075a520b6d5d144a4d44bd43556f Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 5 Aug 2019 13:38:46 -0700 Subject: [PATCH 18/65] Rename SnapshotData to Snapshot --- .../{SnapshotData.lua => Snapshot.lua} | 32 +++++++------- ...napshotData.spec.lua => Snapshot.spec.lua} | 42 +++++++++---------- src/snapshot/Serialize/init.lua | 10 ++--- src/snapshot/SnapshotMatcher.lua | 18 ++++---- src/snapshot/init.lua | 4 +- 5 files changed, 53 insertions(+), 53 deletions(-) rename src/snapshot/Serialize/{SnapshotData.lua => Snapshot.lua} (72%) rename src/snapshot/Serialize/{SnapshotData.spec.lua => Snapshot.spec.lua} (86%) diff --git a/src/snapshot/Serialize/SnapshotData.lua b/src/snapshot/Serialize/Snapshot.lua similarity index 72% rename from src/snapshot/Serialize/SnapshotData.lua rename to src/snapshot/Serialize/Snapshot.lua index 6d5ff0d5..ca2b26b1 100644 --- a/src/snapshot/Serialize/SnapshotData.lua +++ b/src/snapshot/Serialize/Snapshot.lua @@ -8,9 +8,9 @@ local function sortSerializedChildren(childA, childB) return childA.hostKey < childB.hostKey end -local SnapshotData = {} +local Snapshot = {} -function SnapshotData.type(wrapperType) +function Snapshot.type(wrapperType) local typeData = { kind = wrapperType.kind, } @@ -24,7 +24,7 @@ function SnapshotData.type(wrapperType) return typeData end -function SnapshotData.signal(signal) +function Snapshot.signal(signal) local signalToString = tostring(signal) local signalName = signalToString:match("Signal (%w+)") @@ -35,7 +35,7 @@ function SnapshotData.signal(signal) } end -function SnapshotData.propValue(prop) +function Snapshot.propValue(prop) local propType = type(prop) if propType == "string" @@ -48,13 +48,13 @@ function SnapshotData.propValue(prop) return Markers.AnonymousFunction elseif typeof(prop) == "RBXScriptSignal" then - return SnapshotData.signal(prop) + return Snapshot.signal(prop) elseif propType == "userdata" then return prop else - warn(("SnapshotData does not support prop with value %q (type %q)"):format( + warn(("Snapshot does not support prop with value %q (type %q)"):format( tostring(prop), propType )) @@ -62,7 +62,7 @@ function SnapshotData.propValue(prop) end end -function SnapshotData.props(wrapperProps) +function Snapshot.props(wrapperProps) local serializedProps = {} for key, prop in pairs(wrapperProps) do @@ -70,7 +70,7 @@ function SnapshotData.props(wrapperProps) or Type.of(key) == Type.HostChangeEvent or Type.of(key) == Type.HostEvent then - serializedProps[key] = SnapshotData.propValue(prop) + serializedProps[key] = Snapshot.propValue(prop) elseif key == Ref then local current = prop:getValue() @@ -84,7 +84,7 @@ function SnapshotData.props(wrapperProps) end else - error(("SnapshotData does not support prop with key %q (type: %s)"):format( + error(("Snapshot does not support prop with key %q (type: %s)"):format( tostring(key), type(key) )) @@ -94,13 +94,13 @@ function SnapshotData.props(wrapperProps) return serializedProps end -function SnapshotData.children(children) +function Snapshot.children(children) local serializedChildren = {} for i=1, #children do local childWrapper = children[i] - serializedChildren[i] = SnapshotData.wrapper(childWrapper) + serializedChildren[i] = Snapshot.wrapper(childWrapper) end table.sort(serializedChildren, sortSerializedChildren) @@ -108,13 +108,13 @@ function SnapshotData.children(children) return serializedChildren end -function SnapshotData.wrapper(wrapper) +function Snapshot.wrapper(wrapper) return { - type = SnapshotData.type(wrapper.type), + type = Snapshot.type(wrapper.type), hostKey = wrapper.hostKey, - props = SnapshotData.props(wrapper.props), - children = SnapshotData.children(wrapper:getChildren()), + props = Snapshot.props(wrapper.props), + children = Snapshot.children(wrapper:getChildren()), } end -return SnapshotData +return Snapshot diff --git a/src/snapshot/Serialize/SnapshotData.spec.lua b/src/snapshot/Serialize/Snapshot.spec.lua similarity index 86% rename from src/snapshot/Serialize/SnapshotData.spec.lua rename to src/snapshot/Serialize/Snapshot.spec.lua index f19c6809..59d0f0b4 100644 --- a/src/snapshot/Serialize/SnapshotData.spec.lua +++ b/src/snapshot/Serialize/Snapshot.spec.lua @@ -15,7 +15,7 @@ return function() local RobloxRenderer = require(RoactRoot.RobloxRenderer) local ShallowWrapper = require(RoactRoot.ShallowWrapper) - local SnapshotData = require(script.Parent.SnapshotData) + local Snapshot = require(script.Parent.Snapshot) local robloxReconciler = createReconciler(RobloxRenderer) @@ -35,7 +35,7 @@ return function() it("should contain the host kind", function() local wrapper = shallow(createElement("Frame")) - local result = SnapshotData.type(wrapper.type) + local result = Snapshot.type(wrapper.type) expect(result.kind).to.equal(ElementKind.Host) end) @@ -44,7 +44,7 @@ return function() local className = "Frame" local wrapper = shallow(createElement(className)) - local result = SnapshotData.type(wrapper.type) + local result = Snapshot.type(wrapper.type) expect(result.className).to.equal(className) end) @@ -58,7 +58,7 @@ return function() it("should contain the host kind", function() local wrapper = shallow(createElement(SomeComponent)) - local result = SnapshotData.type(wrapper.type) + local result = Snapshot.type(wrapper.type) expect(result.kind).to.equal(ElementKind.Function) end) @@ -75,7 +75,7 @@ return function() it("should contain the host kind", function() local wrapper = shallow(createElement(SomeComponent)) - local result = SnapshotData.type(wrapper.type) + local result = Snapshot.type(wrapper.type) expect(result.kind).to.equal(ElementKind.Stateful) end) @@ -83,7 +83,7 @@ return function() it("should contain the component name", function() local wrapper = shallow(createElement(SomeComponent)) - local result = SnapshotData.type(wrapper.type) + local result = Snapshot.type(wrapper.type) expect(result.componentName).to.equal(componentName) end) @@ -99,7 +99,7 @@ return function() end }) - local result = SnapshotData.signal(signalMock) + local result = Snapshot.signal(signalMock) assertDeepEqual(result, { [Markers.Signal] = signalName @@ -113,20 +113,20 @@ return function() for i=1, #propValues do local prop = propValues[i] - local result = SnapshotData.propValue(prop) + local result = Snapshot.propValue(prop) expect(result).to.equal(prop) end end) it("should return the AnonymousFunction symbol when given a function", function() - local result = SnapshotData.propValue(function() end) + local result = Snapshot.propValue(function() end) expect(result).to.equal(Markers.AnonymousFunction) end) it("should return the Unknown symbol when given an unexpected value", function() - local result = SnapshotData.propValue({}) + local result = Snapshot.propValue({}) expect(result).to.equal(Markers.Unknown) end) @@ -139,7 +139,7 @@ return function() text = "never", } - local result = SnapshotData.props(props) + local result = Snapshot.props(props) assertDeepEqual(result, props) end) @@ -149,7 +149,7 @@ return function() [Event.Activated] = function() end, } - local result = SnapshotData.props(props) + local result = Snapshot.props(props) assertDeepEqual(result, { [Event.Activated] = Markers.AnonymousFunction, @@ -161,7 +161,7 @@ return function() [Change.Position] = function() end, } - local result = SnapshotData.props(props) + local result = Snapshot.props(props) assertDeepEqual(result, { [Change.Position] = Markers.AnonymousFunction, @@ -173,7 +173,7 @@ return function() [Ref] = createRef(), } - local result = SnapshotData.props(props) + local result = Snapshot.props(props) assertDeepEqual(result, { [Ref] = Markers.EmptyRef, @@ -189,7 +189,7 @@ return function() [Ref] = ref, } - local result = SnapshotData.props(props) + local result = Snapshot.props(props) assertDeepEqual(result, { [Ref] = { @@ -200,7 +200,7 @@ return function() it("should throw when the key is a table", function() local function shouldThrow() - SnapshotData.props({ + Snapshot.props({ [{}] = "invalid", }) end @@ -215,7 +215,7 @@ return function() local wrapper = shallow(createElement("Frame")) wrapper.hostKey = hostKey - local result = SnapshotData.wrapper(wrapper) + local result = Snapshot.wrapper(wrapper) expect(result.hostKey).to.equal(hostKey) end) @@ -223,7 +223,7 @@ return function() it("should contain the element type", function() local wrapper = shallow(createElement("Frame")) - local result = SnapshotData.wrapper(wrapper) + local result = Snapshot.wrapper(wrapper) expect(result.type).to.be.ok() expect(result.type.kind).to.equal(ElementKind.Host) @@ -242,7 +242,7 @@ return function() local wrapper = shallow(createElement("Frame", props)) - local result = SnapshotData.wrapper(wrapper) + local result = Snapshot.wrapper(wrapper) expect(result.props).to.be.ok() assertDeepEqual(result.props, expectProps) @@ -253,7 +253,7 @@ return function() Child = createElement("TextLabel"), })) - local result = SnapshotData.wrapper(wrapper) + local result = Snapshot.wrapper(wrapper) expect(result.children).to.be.ok() expect(#result.children).to.equal(1) @@ -268,7 +268,7 @@ return function() Label = createElement("TextLabel"), })) - local result = SnapshotData.wrapper(wrapper) + local result = Snapshot.wrapper(wrapper) expect(result.children).to.be.ok() expect(#result.children).to.equal(2) diff --git a/src/snapshot/Serialize/init.lua b/src/snapshot/Serialize/init.lua index a63178ad..8f30d9aa 100644 --- a/src/snapshot/Serialize/init.lua +++ b/src/snapshot/Serialize/init.lua @@ -1,11 +1,11 @@ local Serializer = require(script.Serializer) -local SnapshotData = require(script.SnapshotData) +local Snapshot = require(script.Snapshot) return { - wrapperToSnapshotData = function(wrapper) - return SnapshotData.wrapper(wrapper) + wrapperToSnapshot = function(wrapper) + return Snapshot.wrapper(wrapper) end, - snapshotDataToString = function(data) - return Serializer.firstSnapshotData(data) + snapshotToString = function(snapshot) + return Serializer.firstSnapshotData(snapshot) end, } diff --git a/src/snapshot/SnapshotMatcher.lua b/src/snapshot/SnapshotMatcher.lua index 93c4ed2e..433e9632 100644 --- a/src/snapshot/SnapshotMatcher.lua +++ b/src/snapshot/SnapshotMatcher.lua @@ -12,15 +12,15 @@ local SnapshotMatcher = {} local SnapshotMetatable = { __index = SnapshotMatcher, __tostring = function(snapshot) - return Serialize.snapshotDataToString(snapshot.data) + return Serialize.snapshotToString(snapshot._snapshot) end } -function SnapshotMatcher.new(identifier, data) +function SnapshotMatcher.new(identifier, snapshot) local snapshot = { _identifier = identifier, - data = data, - _existingData = SnapshotMatcher._loadExistingData(identifier), + _snapshot = snapshot, + _existingSnapshot = SnapshotMatcher._loadExistingData(identifier), } setmetatable(snapshot, SnapshotMetatable) @@ -29,19 +29,19 @@ function SnapshotMatcher.new(identifier, data) end function SnapshotMatcher:match() - if self._existingData == nil then + if self._existingSnapshot == nil then self:serialize() - self._existingData = self.data + self._existingSnapshot = self._snapshot return end - local areEqual, innerMessageTemplate = deepEqual(self.data, self._existingData) + local areEqual, innerMessageTemplate = deepEqual(self._snapshot, self._existingSnapshot) if areEqual then return end - local newSnapshot = SnapshotMatcher.new(self._identifier .. ".NEW", self.data) + local newSnapshot = SnapshotMatcher.new(self._identifier .. ".NEW", self._snapshot) newSnapshot:serialize() local innerMessage = innerMessageTemplate @@ -56,7 +56,7 @@ end function SnapshotMatcher:serialize() local folder = SnapshotMatcher.getSnapshotFolder() - local snapshotSource = Serialize.snapshotDataToString(self.data) + local snapshotSource = Serialize.snapshotToString(self._snapshot) local existingData = folder:FindFirstChild(self._identifier) if not (existingData and existingData:IsA('StringValue')) then diff --git a/src/snapshot/init.lua b/src/snapshot/init.lua index fcfe9caa..5009a7d6 100644 --- a/src/snapshot/init.lua +++ b/src/snapshot/init.lua @@ -10,8 +10,8 @@ return function(identifier, shallowWrapper) error(("Snapshot identifier has invalid character: '%s'"):format(identifier:match(invalidPattern))) end - local data = Serialize.wrapperToSnapshotData(shallowWrapper) - local matcher = SnapshotMatcher.new(identifier, data) + local snapshot = Serialize.wrapperToSnapshot(shallowWrapper) + local matcher = SnapshotMatcher.new(identifier, snapshot) return matcher end From edb1ca397251d89ffd119fc9e885f3be773abe80 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 5 Aug 2019 13:42:41 -0700 Subject: [PATCH 19/65] Fix variable shadowing --- src/snapshot/SnapshotMatcher.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/snapshot/SnapshotMatcher.lua b/src/snapshot/SnapshotMatcher.lua index 433e9632..24b4e199 100644 --- a/src/snapshot/SnapshotMatcher.lua +++ b/src/snapshot/SnapshotMatcher.lua @@ -17,15 +17,15 @@ local SnapshotMetatable = { } function SnapshotMatcher.new(identifier, snapshot) - local snapshot = { + local snapshotMatcher = { _identifier = identifier, _snapshot = snapshot, _existingSnapshot = SnapshotMatcher._loadExistingData(identifier), } - setmetatable(snapshot, SnapshotMetatable) + setmetatable(snapshotMatcher, SnapshotMetatable) - return snapshot + return snapshotMatcher end function SnapshotMatcher:match() From d73ebf11a69ba8040dce7ce83537ff8c51637975 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 5 Aug 2019 14:04:53 -0700 Subject: [PATCH 20/65] Snapshot support table as prop values --- src/snapshot/Serialize/Snapshot.lua | 3 +++ src/snapshot/Serialize/Snapshot.spec.lua | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/snapshot/Serialize/Snapshot.lua b/src/snapshot/Serialize/Snapshot.lua index ca2b26b1..0013eb1b 100644 --- a/src/snapshot/Serialize/Snapshot.lua +++ b/src/snapshot/Serialize/Snapshot.lua @@ -53,6 +53,9 @@ function Snapshot.propValue(prop) elseif propType == "userdata" then return prop + elseif propType == "table" then + return Snapshot.props(prop) + else warn(("Snapshot does not support prop with value %q (type %q)"):format( tostring(prop), diff --git a/src/snapshot/Serialize/Snapshot.spec.lua b/src/snapshot/Serialize/Snapshot.spec.lua index 59d0f0b4..bb079279 100644 --- a/src/snapshot/Serialize/Snapshot.spec.lua +++ b/src/snapshot/Serialize/Snapshot.spec.lua @@ -108,7 +108,7 @@ return function() end) describe("propValue", function() - it("should return the same value", function() + it("should return the same value for basic types", function() local propValues = {7, "hello", Enum.SortOrder.LayoutOrder} for i=1, #propValues do @@ -119,6 +119,23 @@ return function() end end) + it("should return an empty table given an empty table", function() + local result = Snapshot.propValue({}) + + expect(next(result)).never.to.be.ok() + end) + + it("should serialize a table as a props table", function() + local key = "some key" + local value = { + [key] = "foo", + } + local result = Snapshot.propValue(value) + + expect(result[key]).to.equal("foo") + expect(next(result, key)).never.to.be.ok() + end) + it("should return the AnonymousFunction symbol when given a function", function() local result = Snapshot.propValue(function() end) @@ -126,7 +143,7 @@ return function() end) it("should return the Unknown symbol when given an unexpected value", function() - local result = Snapshot.propValue({}) + local result = Snapshot.propValue(nil) expect(result).to.equal(Markers.Unknown) end) From f4844ac2c40b959d4be9e9381725759618dbdf30 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 7 Aug 2019 10:43:38 -0700 Subject: [PATCH 21/65] Move files into a single folder --- src/VirtualTree.lua | 2 +- src/{ => shallow}/ShallowWrapper.lua | 8 +++++--- src/{ => shallow}/ShallowWrapper.spec.lua | 17 +++++++++-------- src/shallow/init.lua | 16 ++++++++++++++++ src/{ => shallow}/snapshot.spec.lua | 14 ++++++++------ .../snapshot/Serialize/IndentedOutput.lua | 0 .../snapshot/Serialize/IndentedOutput.spec.lua | 0 .../Serialize/Markers/AnonymousFunction.lua | 7 +++++++ .../snapshot/Serialize/Markers/EmptyRef.lua | 7 +++++++ .../snapshot/Serialize/Markers/Signal.lua | 7 +++++++ .../snapshot/Serialize/Markers/Unknown.lua | 7 +++++++ .../snapshot/Serialize/Markers/init.lua | 4 +++- .../snapshot/Serialize/Serializer.lua | 3 ++- .../snapshot/Serialize/Serializer.spec.lua | 2 +- .../snapshot/Serialize/Snapshot.lua | 3 ++- .../snapshot/Serialize/Snapshot.spec.lua | 4 ++-- src/{ => shallow}/snapshot/Serialize/init.lua | 0 src/{ => shallow}/snapshot/SnapshotMatcher.lua | 5 +++-- .../snapshot/SnapshotMatcher.spec.lua | 6 ++++-- src/{ => shallow}/snapshot/init.lua | 0 .../Serialize/Markers/AnonymousFunction.lua | 5 ----- src/snapshot/Serialize/Markers/EmptyRef.lua | 5 ----- src/snapshot/Serialize/Markers/Signal.lua | 5 ----- src/snapshot/Serialize/Markers/Unknown.lua | 5 ----- 24 files changed, 84 insertions(+), 48 deletions(-) rename src/{ => shallow}/ShallowWrapper.lua (96%) rename src/{ => shallow}/ShallowWrapper.spec.lua (97%) create mode 100644 src/shallow/init.lua rename src/{ => shallow}/snapshot.spec.lua (87%) rename src/{ => shallow}/snapshot/Serialize/IndentedOutput.lua (100%) rename src/{ => shallow}/snapshot/Serialize/IndentedOutput.spec.lua (100%) create mode 100644 src/shallow/snapshot/Serialize/Markers/AnonymousFunction.lua create mode 100644 src/shallow/snapshot/Serialize/Markers/EmptyRef.lua create mode 100644 src/shallow/snapshot/Serialize/Markers/Signal.lua create mode 100644 src/shallow/snapshot/Serialize/Markers/Unknown.lua rename src/{ => shallow}/snapshot/Serialize/Markers/init.lua (67%) rename src/{ => shallow}/snapshot/Serialize/Serializer.lua (99%) rename src/{ => shallow}/snapshot/Serialize/Serializer.spec.lua (99%) rename src/{ => shallow}/snapshot/Serialize/Snapshot.lua (97%) rename src/{ => shallow}/snapshot/Serialize/Snapshot.spec.lua (98%) rename src/{ => shallow}/snapshot/Serialize/init.lua (100%) rename src/{ => shallow}/snapshot/SnapshotMatcher.lua (94%) rename src/{ => shallow}/snapshot/SnapshotMatcher.spec.lua (95%) rename src/{ => shallow}/snapshot/init.lua (100%) delete mode 100644 src/snapshot/Serialize/Markers/AnonymousFunction.lua delete mode 100644 src/snapshot/Serialize/Markers/EmptyRef.lua delete mode 100644 src/snapshot/Serialize/Markers/Signal.lua delete mode 100644 src/snapshot/Serialize/Markers/Unknown.lua diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua index 8e7c7aa2..f0c182ee 100644 --- a/src/VirtualTree.lua +++ b/src/VirtualTree.lua @@ -1,4 +1,4 @@ -local ShallowWrapper = require(script.Parent.ShallowWrapper) +local ShallowWrapper = require(script.Parent.shallow.ShallowWrapper) local Type = require(script.Parent.Type) local config = require(script.Parent.GlobalConfig).get() diff --git a/src/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua similarity index 96% rename from src/ShallowWrapper.lua rename to src/shallow/ShallowWrapper.lua index 6f768c83..bce25f77 100644 --- a/src/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -1,6 +1,8 @@ -local Children = require(script.Parent.PropMarkers.Children) -local ElementKind = require(script.Parent.ElementKind) -local ElementUtils = require(script.Parent.ElementUtils) +local RoactRoot = script.Parent.Parent + +local Children = require(RoactRoot.PropMarkers.Children) +local ElementKind = require(RoactRoot.ElementKind) +local ElementUtils = require(RoactRoot.ElementUtils) local snapshot = require(script.Parent.snapshot) local ShallowWrapper = {} diff --git a/src/ShallowWrapper.spec.lua b/src/shallow/ShallowWrapper.spec.lua similarity index 97% rename from src/ShallowWrapper.spec.lua rename to src/shallow/ShallowWrapper.spec.lua index 20799734..3df9755e 100644 --- a/src/ShallowWrapper.spec.lua +++ b/src/shallow/ShallowWrapper.spec.lua @@ -1,14 +1,15 @@ return function() + local RoactRoot = script.Parent.Parent local ShallowWrapper = require(script.Parent.ShallowWrapper) - local assertDeepEqual = require(script.Parent.assertDeepEqual) - local Children = require(script.Parent.PropMarkers.Children) - local ElementKind = require(script.Parent.ElementKind) - local createElement = require(script.Parent.createElement) - local createFragment = require(script.Parent.createFragment) - local createReconciler = require(script.Parent.createReconciler) - local RoactComponent = require(script.Parent.Component) - local RobloxRenderer = require(script.Parent.RobloxRenderer) + local assertDeepEqual = require(RoactRoot.assertDeepEqual) + local Children = require(RoactRoot.PropMarkers.Children) + local ElementKind = require(RoactRoot.ElementKind) + local createElement = require(RoactRoot.createElement) + local createFragment = require(RoactRoot.createFragment) + local createReconciler = require(RoactRoot.createReconciler) + local RoactComponent = require(RoactRoot.Component) + local RobloxRenderer = require(RoactRoot.RobloxRenderer) local robloxReconciler = createReconciler(RobloxRenderer) diff --git a/src/shallow/init.lua b/src/shallow/init.lua new file mode 100644 index 00000000..29f9b3d6 --- /dev/null +++ b/src/shallow/init.lua @@ -0,0 +1,16 @@ +local ShallowWrapper = require(script.Parent.ShallowWrapper) + +local function shallow() + options = options or {} + local maxDepth = options.depth or 1 + local internalData = self[internalDataSymbol] + + if config.typeChecks then + assert(Type.of(self) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(internalData.mounted, "Cannot get render output from an unmounted Roact tree") + end + + return ShallowWrapper.new(internalData.rootNode, maxDepth) +end + +return shallow \ No newline at end of file diff --git a/src/snapshot.spec.lua b/src/shallow/snapshot.spec.lua similarity index 87% rename from src/snapshot.spec.lua rename to src/shallow/snapshot.spec.lua index b793363d..39604aa1 100644 --- a/src/snapshot.spec.lua +++ b/src/shallow/snapshot.spec.lua @@ -1,10 +1,12 @@ return function() - local Change = require(script.Parent.PropMarkers.Change) - local Component = require(script.Parent.Component) - local createElement = require(script.Parent.createElement) - local createReconciler = require(script.Parent.createReconciler) - local Event = require(script.Parent.PropMarkers.Event) - local RobloxRenderer = require(script.Parent.RobloxRenderer) + local RoactRoot = script.Parent.Parent + + local Change = require(RoactRoot.PropMarkers.Change) + local Component = require(RoactRoot.Component) + local createElement = require(RoactRoot.createElement) + local createReconciler = require(RoactRoot.createReconciler) + local Event = require(RoactRoot.PropMarkers.Event) + local RobloxRenderer = require(RoactRoot.RobloxRenderer) local snapshot = require(script.Parent.snapshot) local robloxReconciler = createReconciler(RobloxRenderer) diff --git a/src/snapshot/Serialize/IndentedOutput.lua b/src/shallow/snapshot/Serialize/IndentedOutput.lua similarity index 100% rename from src/snapshot/Serialize/IndentedOutput.lua rename to src/shallow/snapshot/Serialize/IndentedOutput.lua diff --git a/src/snapshot/Serialize/IndentedOutput.spec.lua b/src/shallow/snapshot/Serialize/IndentedOutput.spec.lua similarity index 100% rename from src/snapshot/Serialize/IndentedOutput.spec.lua rename to src/shallow/snapshot/Serialize/IndentedOutput.spec.lua diff --git a/src/shallow/snapshot/Serialize/Markers/AnonymousFunction.lua b/src/shallow/snapshot/Serialize/Markers/AnonymousFunction.lua new file mode 100644 index 00000000..4a3c04fa --- /dev/null +++ b/src/shallow/snapshot/Serialize/Markers/AnonymousFunction.lua @@ -0,0 +1,7 @@ +local RoactRoot = script.Parent.Parent.Parent.Parent.Parent + +local Symbol = require(RoactRoot.Symbol) + +local AnonymousFunction = Symbol.named("AnonymousFunction") + +return AnonymousFunction \ No newline at end of file diff --git a/src/shallow/snapshot/Serialize/Markers/EmptyRef.lua b/src/shallow/snapshot/Serialize/Markers/EmptyRef.lua new file mode 100644 index 00000000..53b80650 --- /dev/null +++ b/src/shallow/snapshot/Serialize/Markers/EmptyRef.lua @@ -0,0 +1,7 @@ +local RoactRoot = script.Parent.Parent.Parent.Parent.Parent + +local Symbol = require(RoactRoot.Symbol) + +local EmptyRef = Symbol.named("EmptyRef") + +return EmptyRef \ No newline at end of file diff --git a/src/shallow/snapshot/Serialize/Markers/Signal.lua b/src/shallow/snapshot/Serialize/Markers/Signal.lua new file mode 100644 index 00000000..04244470 --- /dev/null +++ b/src/shallow/snapshot/Serialize/Markers/Signal.lua @@ -0,0 +1,7 @@ +local RoactRoot = script.Parent.Parent.Parent.Parent.Parent + +local Symbol = require(RoactRoot.Symbol) + +local Signal = Symbol.named("Signal") + +return Signal \ No newline at end of file diff --git a/src/shallow/snapshot/Serialize/Markers/Unknown.lua b/src/shallow/snapshot/Serialize/Markers/Unknown.lua new file mode 100644 index 00000000..82ab0d50 --- /dev/null +++ b/src/shallow/snapshot/Serialize/Markers/Unknown.lua @@ -0,0 +1,7 @@ +local RoactRoot = script.Parent.Parent.Parent.Parent.Parent + +local Symbol = require(RoactRoot.Symbol) + +local Unkown = Symbol.named("Unkown") + +return Unkown \ No newline at end of file diff --git a/src/snapshot/Serialize/Markers/init.lua b/src/shallow/snapshot/Serialize/Markers/init.lua similarity index 67% rename from src/snapshot/Serialize/Markers/init.lua rename to src/shallow/snapshot/Serialize/Markers/init.lua index 624661d5..3999927c 100644 --- a/src/snapshot/Serialize/Markers/init.lua +++ b/src/shallow/snapshot/Serialize/Markers/init.lua @@ -1,4 +1,6 @@ -local strict = require(script.Parent.Parent.Parent.strict) +local RoactRoot = script.Parent.Parent.Parent.Parent + +local strict = require(RoactRoot.strict) return strict({ AnonymousFunction = require(script.AnonymousFunction), diff --git a/src/snapshot/Serialize/Serializer.lua b/src/shallow/snapshot/Serialize/Serializer.lua similarity index 99% rename from src/snapshot/Serialize/Serializer.lua rename to src/shallow/snapshot/Serialize/Serializer.lua index 6e22e44f..b74a8537 100644 --- a/src/snapshot/Serialize/Serializer.lua +++ b/src/shallow/snapshot/Serialize/Serializer.lua @@ -1,4 +1,5 @@ -local RoactRoot = script.Parent.Parent.Parent +local RoactRoot = script.Parent.Parent.Parent.Parent + local ElementKind = require(RoactRoot.ElementKind) local Ref = require(RoactRoot.PropMarkers.Ref) local Type = require(RoactRoot.Type) diff --git a/src/snapshot/Serialize/Serializer.spec.lua b/src/shallow/snapshot/Serialize/Serializer.spec.lua similarity index 99% rename from src/snapshot/Serialize/Serializer.spec.lua rename to src/shallow/snapshot/Serialize/Serializer.spec.lua index 786ab5a7..833102ac 100644 --- a/src/snapshot/Serialize/Serializer.spec.lua +++ b/src/shallow/snapshot/Serialize/Serializer.spec.lua @@ -1,5 +1,5 @@ return function() - local RoactRoot = script.Parent.Parent.Parent + local RoactRoot = script.Parent.Parent.Parent.Parent local Markers = require(script.Parent.Markers) local Change = require(RoactRoot.PropMarkers.Change) diff --git a/src/snapshot/Serialize/Snapshot.lua b/src/shallow/snapshot/Serialize/Snapshot.lua similarity index 97% rename from src/snapshot/Serialize/Snapshot.lua rename to src/shallow/snapshot/Serialize/Snapshot.lua index 0013eb1b..65a6fee5 100644 --- a/src/snapshot/Serialize/Snapshot.lua +++ b/src/shallow/snapshot/Serialize/Snapshot.lua @@ -1,4 +1,5 @@ -local RoactRoot = script.Parent.Parent.Parent +local RoactRoot = script.Parent.Parent.Parent.Parent + local Markers = require(script.Parent.Markers) local ElementKind = require(RoactRoot.ElementKind) local Type = require(RoactRoot.Type) diff --git a/src/snapshot/Serialize/Snapshot.spec.lua b/src/shallow/snapshot/Serialize/Snapshot.spec.lua similarity index 98% rename from src/snapshot/Serialize/Snapshot.spec.lua rename to src/shallow/snapshot/Serialize/Snapshot.spec.lua index bb079279..0b98113f 100644 --- a/src/snapshot/Serialize/Snapshot.spec.lua +++ b/src/shallow/snapshot/Serialize/Snapshot.spec.lua @@ -1,5 +1,5 @@ return function() - local RoactRoot = script.Parent.Parent.Parent + local RoactRoot = script.Parent.Parent.Parent.Parent local Markers = require(script.Parent.Markers) local assertDeepEqual = require(RoactRoot.assertDeepEqual) @@ -13,7 +13,7 @@ return function() local Event = require(RoactRoot.PropMarkers.Event) local Ref = require(RoactRoot.PropMarkers.Ref) local RobloxRenderer = require(RoactRoot.RobloxRenderer) - local ShallowWrapper = require(RoactRoot.ShallowWrapper) + local ShallowWrapper = require(script.Parent.Parent.Parent.ShallowWrapper) local Snapshot = require(script.Parent.Snapshot) diff --git a/src/snapshot/Serialize/init.lua b/src/shallow/snapshot/Serialize/init.lua similarity index 100% rename from src/snapshot/Serialize/init.lua rename to src/shallow/snapshot/Serialize/init.lua diff --git a/src/snapshot/SnapshotMatcher.lua b/src/shallow/snapshot/SnapshotMatcher.lua similarity index 94% rename from src/snapshot/SnapshotMatcher.lua rename to src/shallow/snapshot/SnapshotMatcher.lua index 24b4e199..6960b364 100644 --- a/src/snapshot/SnapshotMatcher.lua +++ b/src/shallow/snapshot/SnapshotMatcher.lua @@ -1,9 +1,10 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RoactRoot = script.Parent.Parent.Parent local Markers = require(script.Parent.Serialize.Markers) local Serialize = require(script.Parent.Serialize) -local deepEqual = require(script.Parent.Parent.deepEqual) -local ElementKind = require(script.Parent.Parent.ElementKind) +local deepEqual = require(RoactRoot.deepEqual) +local ElementKind = require(RoactRoot.ElementKind) local SnapshotFolderName = "RoactSnapshots" local SnapshotFolder = ReplicatedStorage:FindFirstChild(SnapshotFolderName) diff --git a/src/snapshot/SnapshotMatcher.spec.lua b/src/shallow/snapshot/SnapshotMatcher.spec.lua similarity index 95% rename from src/snapshot/SnapshotMatcher.spec.lua rename to src/shallow/snapshot/SnapshotMatcher.spec.lua index ec7a2042..be44e88f 100644 --- a/src/snapshot/SnapshotMatcher.spec.lua +++ b/src/shallow/snapshot/SnapshotMatcher.spec.lua @@ -1,8 +1,10 @@ return function() + local RoactRoot = script.Parent.Parent.Parent + local SnapshotMatcher = require(script.Parent.SnapshotMatcher) - local ElementKind = require(script.Parent.Parent.ElementKind) - local createSpy = require(script.Parent.Parent.createSpy) + local ElementKind = require(RoactRoot.ElementKind) + local createSpy = require(RoactRoot.createSpy) local snapshotFolder = Instance.new("Folder") local originalGetSnapshotFolder = SnapshotMatcher.getSnapshotFolder diff --git a/src/snapshot/init.lua b/src/shallow/snapshot/init.lua similarity index 100% rename from src/snapshot/init.lua rename to src/shallow/snapshot/init.lua diff --git a/src/snapshot/Serialize/Markers/AnonymousFunction.lua b/src/snapshot/Serialize/Markers/AnonymousFunction.lua deleted file mode 100644 index a5c1abca..00000000 --- a/src/snapshot/Serialize/Markers/AnonymousFunction.lua +++ /dev/null @@ -1,5 +0,0 @@ -local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) - -local AnonymousFunction = Symbol.named("AnonymousFunction") - -return AnonymousFunction \ No newline at end of file diff --git a/src/snapshot/Serialize/Markers/EmptyRef.lua b/src/snapshot/Serialize/Markers/EmptyRef.lua deleted file mode 100644 index cf5e193f..00000000 --- a/src/snapshot/Serialize/Markers/EmptyRef.lua +++ /dev/null @@ -1,5 +0,0 @@ -local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) - -local EmptyRef = Symbol.named("EmptyRef") - -return EmptyRef \ No newline at end of file diff --git a/src/snapshot/Serialize/Markers/Signal.lua b/src/snapshot/Serialize/Markers/Signal.lua deleted file mode 100644 index 9e9285b0..00000000 --- a/src/snapshot/Serialize/Markers/Signal.lua +++ /dev/null @@ -1,5 +0,0 @@ -local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) - -local Signal = Symbol.named("Signal") - -return Signal \ No newline at end of file diff --git a/src/snapshot/Serialize/Markers/Unknown.lua b/src/snapshot/Serialize/Markers/Unknown.lua deleted file mode 100644 index 24f8c48d..00000000 --- a/src/snapshot/Serialize/Markers/Unknown.lua +++ /dev/null @@ -1,5 +0,0 @@ -local Symbol = require(script.Parent.Parent.Parent.Parent.Symbol) - -local Unkown = Symbol.named("Unkown") - -return Unkown \ No newline at end of file From a541ec45849c62571de63817a74e661d01cd3d77 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 7 Aug 2019 15:00:39 -0700 Subject: [PATCH 22/65] Revert VirtualTree --- src/VirtualTree.lua | 33 --------------------------------- src/VirtualTree.spec.lua | 24 ------------------------ src/createReconciler.lua | 15 ++++++++++++--- src/shallow.spec.lua | 17 +++++++++++++++++ src/shallow/init.lua | 21 +++++++++++++-------- src/shallow/snapshot.spec.lua | 19 +++++++++++-------- 6 files changed, 53 insertions(+), 76 deletions(-) delete mode 100644 src/VirtualTree.lua delete mode 100644 src/VirtualTree.spec.lua create mode 100644 src/shallow.spec.lua diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua deleted file mode 100644 index f0c182ee..00000000 --- a/src/VirtualTree.lua +++ /dev/null @@ -1,33 +0,0 @@ -local ShallowWrapper = require(script.Parent.shallow.ShallowWrapper) -local Type = require(script.Parent.Type) - -local config = require(script.Parent.GlobalConfig).get() - -local VirtualTree = {} - -function VirtualTree.new(rootNode, internalDataSymbol, mounted) - local tree = { - [Type] = Type.VirtualTree, - [internalDataSymbol] = { - rootNode = rootNode, - mounted = mounted, - } - } - - function tree:getTestRenderOutput(options) - options = options or {} - local maxDepth = options.depth or 1 - local internalData = self[internalDataSymbol] - - if config.typeChecks then - assert(Type.of(self) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") - assert(internalData.mounted, "Cannot get render output from an unmounted Roact tree") - end - - return ShallowWrapper.new(internalData.rootNode, maxDepth) - end - - return tree -end - -return VirtualTree \ No newline at end of file diff --git a/src/VirtualTree.spec.lua b/src/VirtualTree.spec.lua deleted file mode 100644 index 764a4ce3..00000000 --- a/src/VirtualTree.spec.lua +++ /dev/null @@ -1,24 +0,0 @@ -return function() - local createElement = require(script.Parent.createElement) - local createReconciler = require(script.Parent.createReconciler) - local RobloxRenderer = require(script.Parent.RobloxRenderer) - - local robloxReconciler = createReconciler(RobloxRenderer) - - describe("getTestRenderOutput", function() - it("should return a ShallowWrapper with the given depth", function() - local function Component() - return createElement("Frame") - end - local element = createElement(Component) - - local tree = robloxReconciler.mountVirtualTree(element) - - local wrapper = tree:getTestRenderOutput({ - depth = 0, - }) - - expect(wrapper.type.functionComponent).to.equal(Component) - end) - end) -end \ No newline at end of file diff --git a/src/createReconciler.lua b/src/createReconciler.lua index c207b911..bcab1ba2 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -4,7 +4,6 @@ local ElementUtils = require(script.Parent.ElementUtils) local Children = require(script.Parent.PropMarkers.Children) local Symbol = require(script.Parent.Symbol) local internalAssert = require(script.Parent.internalAssert) -local VirtualTree = require(script.Parent.VirtualTree) local config = require(script.Parent.GlobalConfig).get() @@ -360,9 +359,19 @@ local function createReconciler(renderer) hostKey = "RoactTree" end - local rootNode = mountVirtualNode(element, hostParent, hostKey) + local tree = { + [Type] = Type.VirtualTree, + [InternalData] = { + -- The root node of the tree, which starts into the hierarchy of + -- Roact component instances. + rootNode = nil, + mounted = true, + }, + } + + tree[InternalData].rootNode = mountVirtualNode(element, hostParent, hostKey) - return VirtualTree.new(rootNode, InternalData, true) + return tree end --[[ diff --git a/src/shallow.spec.lua b/src/shallow.spec.lua new file mode 100644 index 00000000..17f3424c --- /dev/null +++ b/src/shallow.spec.lua @@ -0,0 +1,17 @@ +return function() + local createElement = require(script.Parent.createElement) + local shallow = require(script.Parent.shallow) + + it("should return a shallow wrapper with depth = 1 by default", function() + local element = createElement("Frame", {}, { + Child = createElement("Frame", {}, { + SubChild = createElement("Frame"), + }), + }) + + local wrapper = shallow(element) + local childWrapper = wrapper:findUnique() + + expect(childWrapper:childrenCount()).to.equal(0) + end) +end \ No newline at end of file diff --git a/src/shallow/init.lua b/src/shallow/init.lua index 29f9b3d6..e4dd6b9f 100644 --- a/src/shallow/init.lua +++ b/src/shallow/init.lua @@ -1,16 +1,21 @@ -local ShallowWrapper = require(script.Parent.ShallowWrapper) +local createReconciler = require(script.Parent.createReconciler) +local Type = require(script.Parent.Type) +local RobloxRenderer = require(script.Parent.RobloxRenderer) +local ShallowWrapper = require(script.ShallowWrapper) + +local robloxReconciler = createReconciler(RobloxRenderer) + +local shallowTreeKey = "RoactTree" + +local function shallow(element, options) + assert(Type.of(element) == Type.Element, "Expected arg #1 to be an Element") -local function shallow() options = options or {} local maxDepth = options.depth or 1 - local internalData = self[internalDataSymbol] - if config.typeChecks then - assert(Type.of(self) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") - assert(internalData.mounted, "Cannot get render output from an unmounted Roact tree") - end + local rootNode = robloxReconciler.mountVirtualNode(element, nil, shallowTreeKey) - return ShallowWrapper.new(internalData.rootNode, maxDepth) + return ShallowWrapper.new(rootNode, maxDepth) end return shallow \ No newline at end of file diff --git a/src/shallow/snapshot.spec.lua b/src/shallow/snapshot.spec.lua index 39604aa1..0b879076 100644 --- a/src/shallow/snapshot.spec.lua +++ b/src/shallow/snapshot.spec.lua @@ -7,10 +7,13 @@ return function() local createReconciler = require(RoactRoot.createReconciler) local Event = require(RoactRoot.PropMarkers.Event) local RobloxRenderer = require(RoactRoot.RobloxRenderer) + local ShallowWrapper = require(script.Parent.ShallowWrapper) local snapshot = require(script.Parent.snapshot) local robloxReconciler = createReconciler(RobloxRenderer) + local hostTreeKey = "RoactTree" + it("should match snapshot of host component with multiple props", function() local element = createElement("Frame", { AnchorPoint = Vector2.new(0, 0.5), @@ -23,8 +26,8 @@ return function() ZIndex = 5, }) - local tree = robloxReconciler.mountVirtualTree(element) - local wrapper = tree:getTestRenderOutput() + local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) + local wrapper = ShallowWrapper.new(rootNode, 1) snapshot("host-frame-with-multiple-props", wrapper):match() end) @@ -43,8 +46,8 @@ return function() }), }) - local tree = robloxReconciler.mountVirtualTree(element) - local wrapper = tree:getTestRenderOutput() + local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) + local wrapper = ShallowWrapper.new(rootNode, 1) snapshot("function-component-children", wrapper):match() end) @@ -64,8 +67,8 @@ return function() }), }) - local tree = robloxReconciler.mountVirtualTree(element) - local wrapper = tree:getTestRenderOutput() + local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) + local wrapper = ShallowWrapper.new(rootNode, 1) snapshot("stateful-component-children", wrapper):match() end) @@ -81,8 +84,8 @@ return function() [Event.MouseButton1Click] = emptyFunction, }) - local tree = robloxReconciler.mountVirtualTree(element) - local wrapper = tree:getTestRenderOutput() + local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) + local wrapper = ShallowWrapper.new(rootNode, 1) snapshot("component-with-event-props", wrapper):match() end) From 87337956b3e4b9a8cf882e0d64f144b6f56a5605 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 7 Aug 2019 15:07:32 -0700 Subject: [PATCH 23/65] Rename Snapshot constructor --- src/shallow/snapshot/Serialize/Snapshot.lua | 4 ++-- src/shallow/snapshot/Serialize/Snapshot.spec.lua | 10 +++++----- src/shallow/snapshot/Serialize/init.lua | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/shallow/snapshot/Serialize/Snapshot.lua b/src/shallow/snapshot/Serialize/Snapshot.lua index 65a6fee5..137934e6 100644 --- a/src/shallow/snapshot/Serialize/Snapshot.lua +++ b/src/shallow/snapshot/Serialize/Snapshot.lua @@ -104,7 +104,7 @@ function Snapshot.children(children) for i=1, #children do local childWrapper = children[i] - serializedChildren[i] = Snapshot.wrapper(childWrapper) + serializedChildren[i] = Snapshot.new(childWrapper) end table.sort(serializedChildren, sortSerializedChildren) @@ -112,7 +112,7 @@ function Snapshot.children(children) return serializedChildren end -function Snapshot.wrapper(wrapper) +function Snapshot.new(wrapper) return { type = Snapshot.type(wrapper.type), hostKey = wrapper.hostKey, diff --git a/src/shallow/snapshot/Serialize/Snapshot.spec.lua b/src/shallow/snapshot/Serialize/Snapshot.spec.lua index 0b98113f..551ebd4a 100644 --- a/src/shallow/snapshot/Serialize/Snapshot.spec.lua +++ b/src/shallow/snapshot/Serialize/Snapshot.spec.lua @@ -232,7 +232,7 @@ return function() local wrapper = shallow(createElement("Frame")) wrapper.hostKey = hostKey - local result = Snapshot.wrapper(wrapper) + local result = Snapshot.new(wrapper) expect(result.hostKey).to.equal(hostKey) end) @@ -240,7 +240,7 @@ return function() it("should contain the element type", function() local wrapper = shallow(createElement("Frame")) - local result = Snapshot.wrapper(wrapper) + local result = Snapshot.new(wrapper) expect(result.type).to.be.ok() expect(result.type.kind).to.equal(ElementKind.Host) @@ -259,7 +259,7 @@ return function() local wrapper = shallow(createElement("Frame", props)) - local result = Snapshot.wrapper(wrapper) + local result = Snapshot.new(wrapper) expect(result.props).to.be.ok() assertDeepEqual(result.props, expectProps) @@ -270,7 +270,7 @@ return function() Child = createElement("TextLabel"), })) - local result = Snapshot.wrapper(wrapper) + local result = Snapshot.new(wrapper) expect(result.children).to.be.ok() expect(#result.children).to.equal(1) @@ -285,7 +285,7 @@ return function() Label = createElement("TextLabel"), })) - local result = Snapshot.wrapper(wrapper) + local result = Snapshot.new(wrapper) expect(result.children).to.be.ok() expect(#result.children).to.equal(2) diff --git a/src/shallow/snapshot/Serialize/init.lua b/src/shallow/snapshot/Serialize/init.lua index 8f30d9aa..170cca39 100644 --- a/src/shallow/snapshot/Serialize/init.lua +++ b/src/shallow/snapshot/Serialize/init.lua @@ -3,7 +3,7 @@ local Snapshot = require(script.Snapshot) return { wrapperToSnapshot = function(wrapper) - return Snapshot.wrapper(wrapper) + return Snapshot.new(wrapper) end, snapshotToString = function(snapshot) return Serializer.firstSnapshotData(snapshot) From 1b8e6e88e4975a7746598dece068cac831c1e656 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 7 Aug 2019 15:36:38 -0700 Subject: [PATCH 24/65] Add API to get snapshot string --- src/shallow/ShallowWrapper.lua | 10 +++++++++- src/shallow/snapshot/SnapshotMatcher.lua | 9 +++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index bce25f77..a6aac873 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -205,7 +205,7 @@ function ShallowWrapper:getChildren() return results end -function ShallowWrapper:toMatchSnapshot(identifier) +function ShallowWrapper:matchSnapshot(identifier) assert(typeof(identifier) == "string", "Snapshot identifier must be a string") local snapshotResult = snapshot(identifier, self) @@ -213,6 +213,14 @@ function ShallowWrapper:toMatchSnapshot(identifier) snapshotResult:match() end +function ShallowWrapper:getSnapshotString(identifier) + assert(typeof(identifier) == "string", "Snapshot identifier must be a string") + + local snapshotResult = snapshot(identifier, self) + + return snapshotResult:getSnapshotString() +end + function ShallowWrapper:_satisfiesAllContraints(constraints) local virtualNode = self._virtualNode diff --git a/src/shallow/snapshot/SnapshotMatcher.lua b/src/shallow/snapshot/SnapshotMatcher.lua index 6960b364..7a87eef0 100644 --- a/src/shallow/snapshot/SnapshotMatcher.lua +++ b/src/shallow/snapshot/SnapshotMatcher.lua @@ -12,9 +12,6 @@ local SnapshotFolder = ReplicatedStorage:FindFirstChild(SnapshotFolderName) local SnapshotMatcher = {} local SnapshotMetatable = { __index = SnapshotMatcher, - __tostring = function(snapshot) - return Serialize.snapshotToString(snapshot._snapshot) - end } function SnapshotMatcher.new(identifier, snapshot) @@ -54,10 +51,14 @@ function SnapshotMatcher:match() error(message, 2) end +function SnapshotMatcher:getSnapshotString() + return Serialize.snapshotToString(self._snapshot) +end + function SnapshotMatcher:serialize() local folder = SnapshotMatcher.getSnapshotFolder() - local snapshotSource = Serialize.snapshotToString(self._snapshot) + local snapshotSource = self:getSnapshotString() local existingData = folder:FindFirstChild(self._identifier) if not (existingData and existingData:IsA('StringValue')) then From 0a187d99604048de723c511d0bc0d5d0b3bde8d3 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 7 Aug 2019 15:37:44 -0700 Subject: [PATCH 25/65] Remove children caching on ShallowWrapper --- src/shallow/ShallowWrapper.lua | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index a6aac873..9dd681b0 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -128,7 +128,6 @@ function ShallowWrapper.new(virtualNode, maxDepth) _virtualNode = virtualNode, _childrenMaxDepth = maxDepth - 1, _virtualNodeChildren = maxDepth == 0 and {} or virtualNode.children, - _shallowChildren = nil, type = getTypeFromVirtualNode(virtualNode), props = filterProps(virtualNode.currentElement.props), hostKey = virtualNode.hostKey, @@ -191,17 +190,12 @@ function ShallowWrapper:findUnique(constraints) end function ShallowWrapper:getChildren() - if self._shallowChildren then - return self._shallowChildren - end - local results = {} for _, childVirtualNode in pairs(self._virtualNodeChildren) do getChildren(childVirtualNode, results, self._childrenMaxDepth) end - self._shallowChildren = results return results end From a843b91857f241e14077c8ea617b4a7feb91cf78 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 7 Aug 2019 15:46:50 -0700 Subject: [PATCH 26/65] Add shallow to global api --- src/init.lua | 2 ++ src/init.spec.lua | 1 + src/{shallow.spec.lua => shallow/init.spec.lua} | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) rename src/{shallow.spec.lua => shallow/init.spec.lua} (77%) diff --git a/src/init.lua b/src/init.lua index f002f975..8e9240f4 100644 --- a/src/init.lua +++ b/src/init.lua @@ -8,6 +8,7 @@ local createReconcilerCompat = require(script.createReconcilerCompat) local RobloxRenderer = require(script.RobloxRenderer) local strict = require(script.strict) local Binding = require(script.Binding) +local shallow = require(script.shallow) local robloxReconciler = createReconciler(RobloxRenderer) local reconcilerCompat = createReconcilerCompat(robloxReconciler) @@ -32,6 +33,7 @@ local Roact = strict { mount = robloxReconciler.mountVirtualTree, unmount = robloxReconciler.unmountVirtualTree, update = robloxReconciler.updateVirtualTree, + shallow = shallow, reify = reconcilerCompat.reify, teardown = reconcilerCompat.teardown, diff --git a/src/init.spec.lua b/src/init.spec.lua index 7fcf79c8..439620a6 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -11,6 +11,7 @@ return function() mount = "function", unmount = "function", update = "function", + shallow = "function", oneChild = "function", setGlobalConfig = "function", diff --git a/src/shallow.spec.lua b/src/shallow/init.spec.lua similarity index 77% rename from src/shallow.spec.lua rename to src/shallow/init.spec.lua index 17f3424c..10e0ee65 100644 --- a/src/shallow.spec.lua +++ b/src/shallow/init.spec.lua @@ -1,6 +1,6 @@ return function() - local createElement = require(script.Parent.createElement) - local shallow = require(script.Parent.shallow) + local createElement = require(script.Parent.Parent.createElement) + local shallow = require(script.Parent) it("should return a shallow wrapper with depth = 1 by default", function() local element = createElement("Frame", {}, { From caca4edc3ae7087595d29f2879b68ef4c3d47783 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 7 Aug 2019 18:20:59 -0700 Subject: [PATCH 27/65] Fix format issue --- src/shallow/ShallowWrapper.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 9dd681b0..efef010a 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -149,7 +149,7 @@ function ShallowWrapper:childrenCount() end function ShallowWrapper:find(constraints) - for constraint in pairs(constraints) do + for constraint in pairs(constraints) do if not ContraintFunctions[constraint] then error(('unknown constraint %q'):format(constraint)) end From f1da1a23fc027f4ef07ffc0e39ccb87d96850ef4 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 8 Aug 2019 10:51:20 -0700 Subject: [PATCH 28/65] Rename snapshot to string functions --- src/shallow/ShallowWrapper.lua | 12 ++++-------- src/shallow/snapshot/SnapshotMatcher.lua | 10 +++------- src/shallow/snapshot/init.lua | 13 ++++++++++++- .../init.spec.lua} | 16 ++++++++-------- 4 files changed, 27 insertions(+), 24 deletions(-) rename src/shallow/{snapshot.spec.lua => snapshot/init.spec.lua} (83%) diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index efef010a..24c0b96f 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -3,7 +3,7 @@ local RoactRoot = script.Parent.Parent local Children = require(RoactRoot.PropMarkers.Children) local ElementKind = require(RoactRoot.ElementKind) local ElementUtils = require(RoactRoot.ElementUtils) -local snapshot = require(script.Parent.snapshot) +local Snapshot = require(script.Parent.Snapshot) local ShallowWrapper = {} local ShallowWrapperMetatable = { @@ -202,17 +202,13 @@ end function ShallowWrapper:matchSnapshot(identifier) assert(typeof(identifier) == "string", "Snapshot identifier must be a string") - local snapshotResult = snapshot(identifier, self) + local snapshotResult = Snapshot.createMatcher(identifier, self) snapshotResult:match() end -function ShallowWrapper:getSnapshotString(identifier) - assert(typeof(identifier) == "string", "Snapshot identifier must be a string") - - local snapshotResult = snapshot(identifier, self) - - return snapshotResult:getSnapshotString() +function ShallowWrapper:snapshotToString() + return Snapshot.toString(self) end function ShallowWrapper:_satisfiesAllContraints(constraints) diff --git a/src/shallow/snapshot/SnapshotMatcher.lua b/src/shallow/snapshot/SnapshotMatcher.lua index 7a87eef0..1560bbd7 100644 --- a/src/shallow/snapshot/SnapshotMatcher.lua +++ b/src/shallow/snapshot/SnapshotMatcher.lua @@ -51,17 +51,13 @@ function SnapshotMatcher:match() error(message, 2) end -function SnapshotMatcher:getSnapshotString() - return Serialize.snapshotToString(self._snapshot) -end - function SnapshotMatcher:serialize() local folder = SnapshotMatcher.getSnapshotFolder() - local snapshotSource = self:getSnapshotString() + local snapshotSource = Serialize.snapshotToString(self._snapshot) local existingData = folder:FindFirstChild(self._identifier) - if not (existingData and existingData:IsA('StringValue')) then + if not (existingData and existingData:IsA("StringValue")) then existingData = Instance.new("StringValue") existingData.Name = self._identifier existingData.Parent = folder @@ -94,7 +90,7 @@ function SnapshotMatcher._loadExistingData(identifier) local loadSnapshot = require(existingData) return loadSnapshot({ - Roact = require(script.Parent.Parent), + Roact = require(RoactRoot), ElementKind = ElementKind, Markers = Markers, }) diff --git a/src/shallow/snapshot/init.lua b/src/shallow/snapshot/init.lua index 5009a7d6..88b5ea84 100644 --- a/src/shallow/snapshot/init.lua +++ b/src/shallow/snapshot/init.lua @@ -5,7 +5,7 @@ local characterClass = "%w_%-%." local identifierPattern = "^[" .. characterClass .. "]+$" local invalidPattern = "[^" .. characterClass .. "]" -return function(identifier, shallowWrapper) +local function createMatcher(identifier, shallowWrapper) if not identifier:match(identifierPattern) then error(("Snapshot identifier has invalid character: '%s'"):format(identifier:match(invalidPattern))) end @@ -15,3 +15,14 @@ return function(identifier, shallowWrapper) return matcher end + +local function toString(shallowWrapper) + local snapshot = Serialize.wrapperToSnapshot(shallowWrapper) + + return Serialize.snapshotToString(snapshot) +end + +return { + createMatcher = createMatcher, + toString = toString, +} \ No newline at end of file diff --git a/src/shallow/snapshot.spec.lua b/src/shallow/snapshot/init.spec.lua similarity index 83% rename from src/shallow/snapshot.spec.lua rename to src/shallow/snapshot/init.spec.lua index 0b879076..69fb22e1 100644 --- a/src/shallow/snapshot.spec.lua +++ b/src/shallow/snapshot/init.spec.lua @@ -1,5 +1,5 @@ return function() - local RoactRoot = script.Parent.Parent + local RoactRoot = script.Parent.Parent.Parent local Change = require(RoactRoot.PropMarkers.Change) local Component = require(RoactRoot.Component) @@ -7,8 +7,8 @@ return function() local createReconciler = require(RoactRoot.createReconciler) local Event = require(RoactRoot.PropMarkers.Event) local RobloxRenderer = require(RoactRoot.RobloxRenderer) - local ShallowWrapper = require(script.Parent.ShallowWrapper) - local snapshot = require(script.Parent.snapshot) + local ShallowWrapper = require(script.Parent.Parent.ShallowWrapper) + local Snapshot = require(script.Parent) local robloxReconciler = createReconciler(RobloxRenderer) @@ -29,7 +29,7 @@ return function() local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) local wrapper = ShallowWrapper.new(rootNode, 1) - snapshot("host-frame-with-multiple-props", wrapper):match() + Snapshot.createMatcher("host-frame-with-multiple-props", wrapper):match() end) it("should match snapshot of function component children", function() @@ -49,7 +49,7 @@ return function() local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) local wrapper = ShallowWrapper.new(rootNode, 1) - snapshot("function-component-children", wrapper):match() + Snapshot.createMatcher("function-component-children", wrapper):match() end) it("should match snapshot of stateful component", function() @@ -70,7 +70,7 @@ return function() local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) local wrapper = ShallowWrapper.new(rootNode, 1) - snapshot("stateful-component-children", wrapper):match() + Snapshot.createMatcher("stateful-component-children", wrapper):match() end) it("should match snapshot with event props", function() @@ -87,7 +87,7 @@ return function() local rootNode = robloxReconciler.mountVirtualNode(element, nil, hostTreeKey) local wrapper = ShallowWrapper.new(rootNode, 1) - snapshot("component-with-event-props", wrapper):match() + Snapshot.createMatcher("component-with-event-props", wrapper):match() end) it("should throw if the identifier contains invalid characters", function() @@ -95,7 +95,7 @@ return function() for i=1, #invalidCharacters do local function shouldThrow() - snapshot("id" .. invalidCharacters[i], {}) + Snapshot.createMatcher("id" .. invalidCharacters[i], {}) end expect(shouldThrow).to.throw() From 8dcd09683471cc39879806521839838cb66ecb03 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 8 Aug 2019 11:04:19 -0700 Subject: [PATCH 29/65] Fix folder casing --- src/shallow/{snapshot => Snapshot}/Serialize/IndentedOutput.lua | 0 .../{snapshot => Snapshot}/Serialize/IndentedOutput.spec.lua | 0 .../Serialize/Markers/AnonymousFunction.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Markers/EmptyRef.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Markers/Signal.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Markers/Unknown.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Markers/init.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Serializer.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Serializer.spec.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Snapshot.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/Snapshot.spec.lua | 0 src/shallow/{snapshot => Snapshot}/Serialize/init.lua | 0 src/shallow/{snapshot => Snapshot}/SnapshotMatcher.lua | 0 src/shallow/{snapshot => Snapshot}/SnapshotMatcher.spec.lua | 0 src/shallow/{snapshot => Snapshot}/init.lua | 0 src/shallow/{snapshot => Snapshot}/init.spec.lua | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename src/shallow/{snapshot => Snapshot}/Serialize/IndentedOutput.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/IndentedOutput.spec.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Markers/AnonymousFunction.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Markers/EmptyRef.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Markers/Signal.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Markers/Unknown.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Markers/init.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Serializer.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Serializer.spec.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Snapshot.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/Snapshot.spec.lua (100%) rename src/shallow/{snapshot => Snapshot}/Serialize/init.lua (100%) rename src/shallow/{snapshot => Snapshot}/SnapshotMatcher.lua (100%) rename src/shallow/{snapshot => Snapshot}/SnapshotMatcher.spec.lua (100%) rename src/shallow/{snapshot => Snapshot}/init.lua (100%) rename src/shallow/{snapshot => Snapshot}/init.spec.lua (100%) diff --git a/src/shallow/snapshot/Serialize/IndentedOutput.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.lua similarity index 100% rename from src/shallow/snapshot/Serialize/IndentedOutput.lua rename to src/shallow/Snapshot/Serialize/IndentedOutput.lua diff --git a/src/shallow/snapshot/Serialize/IndentedOutput.spec.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua similarity index 100% rename from src/shallow/snapshot/Serialize/IndentedOutput.spec.lua rename to src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua diff --git a/src/shallow/snapshot/Serialize/Markers/AnonymousFunction.lua b/src/shallow/Snapshot/Serialize/Markers/AnonymousFunction.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Markers/AnonymousFunction.lua rename to src/shallow/Snapshot/Serialize/Markers/AnonymousFunction.lua diff --git a/src/shallow/snapshot/Serialize/Markers/EmptyRef.lua b/src/shallow/Snapshot/Serialize/Markers/EmptyRef.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Markers/EmptyRef.lua rename to src/shallow/Snapshot/Serialize/Markers/EmptyRef.lua diff --git a/src/shallow/snapshot/Serialize/Markers/Signal.lua b/src/shallow/Snapshot/Serialize/Markers/Signal.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Markers/Signal.lua rename to src/shallow/Snapshot/Serialize/Markers/Signal.lua diff --git a/src/shallow/snapshot/Serialize/Markers/Unknown.lua b/src/shallow/Snapshot/Serialize/Markers/Unknown.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Markers/Unknown.lua rename to src/shallow/Snapshot/Serialize/Markers/Unknown.lua diff --git a/src/shallow/snapshot/Serialize/Markers/init.lua b/src/shallow/Snapshot/Serialize/Markers/init.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Markers/init.lua rename to src/shallow/Snapshot/Serialize/Markers/init.lua diff --git a/src/shallow/snapshot/Serialize/Serializer.lua b/src/shallow/Snapshot/Serialize/Serializer.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Serializer.lua rename to src/shallow/Snapshot/Serialize/Serializer.lua diff --git a/src/shallow/snapshot/Serialize/Serializer.spec.lua b/src/shallow/Snapshot/Serialize/Serializer.spec.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Serializer.spec.lua rename to src/shallow/Snapshot/Serialize/Serializer.spec.lua diff --git a/src/shallow/snapshot/Serialize/Snapshot.lua b/src/shallow/Snapshot/Serialize/Snapshot.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Snapshot.lua rename to src/shallow/Snapshot/Serialize/Snapshot.lua diff --git a/src/shallow/snapshot/Serialize/Snapshot.spec.lua b/src/shallow/Snapshot/Serialize/Snapshot.spec.lua similarity index 100% rename from src/shallow/snapshot/Serialize/Snapshot.spec.lua rename to src/shallow/Snapshot/Serialize/Snapshot.spec.lua diff --git a/src/shallow/snapshot/Serialize/init.lua b/src/shallow/Snapshot/Serialize/init.lua similarity index 100% rename from src/shallow/snapshot/Serialize/init.lua rename to src/shallow/Snapshot/Serialize/init.lua diff --git a/src/shallow/snapshot/SnapshotMatcher.lua b/src/shallow/Snapshot/SnapshotMatcher.lua similarity index 100% rename from src/shallow/snapshot/SnapshotMatcher.lua rename to src/shallow/Snapshot/SnapshotMatcher.lua diff --git a/src/shallow/snapshot/SnapshotMatcher.spec.lua b/src/shallow/Snapshot/SnapshotMatcher.spec.lua similarity index 100% rename from src/shallow/snapshot/SnapshotMatcher.spec.lua rename to src/shallow/Snapshot/SnapshotMatcher.spec.lua diff --git a/src/shallow/snapshot/init.lua b/src/shallow/Snapshot/init.lua similarity index 100% rename from src/shallow/snapshot/init.lua rename to src/shallow/Snapshot/init.lua diff --git a/src/shallow/snapshot/init.spec.lua b/src/shallow/Snapshot/init.spec.lua similarity index 100% rename from src/shallow/snapshot/init.spec.lua rename to src/shallow/Snapshot/init.spec.lua From 6bd217eb7b081a92aedd01243a73899bcd4e8174 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 8 Aug 2019 16:45:27 -0700 Subject: [PATCH 30/65] Refactor constraints to be self contained --- src/shallow/ShallowWrapper.lua | 52 +-- src/shallow/ShallowWrapper.spec.lua | 309 ------------------ .../VirtualNodeConstraints/Constraints.lua | 42 +++ .../Constraints.spec.lua | 200 ++++++++++++ src/shallow/VirtualNodeConstraints/init.lua | 24 ++ .../VirtualNodeConstraints/init.spec.lua | 31 ++ 6 files changed, 301 insertions(+), 357 deletions(-) create mode 100644 src/shallow/VirtualNodeConstraints/Constraints.lua create mode 100644 src/shallow/VirtualNodeConstraints/Constraints.spec.lua create mode 100644 src/shallow/VirtualNodeConstraints/init.lua create mode 100644 src/shallow/VirtualNodeConstraints/init.spec.lua diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 24c0b96f..2961f941 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -3,6 +3,7 @@ local RoactRoot = script.Parent.Parent local Children = require(RoactRoot.PropMarkers.Children) local ElementKind = require(RoactRoot.ElementKind) local ElementUtils = require(RoactRoot.ElementUtils) +local VirtualNodeConstraints = require(script.Parent.VirtualNodeConstraints) local Snapshot = require(script.Parent.Snapshot) local ShallowWrapper = {} @@ -48,34 +49,6 @@ local function findNextVirtualNode(virtualNode, maxDepth) return currentNode end -local ContraintFunctions = { - kind = function(virtualNode, expectKind) - return ElementKind.of(virtualNode.currentElement) == expectKind - end, - className = function(virtualNode, className) - local element = virtualNode.currentElement - local isHost = ElementKind.of(element) == ElementKind.Host - return isHost and element.component == className - end, - component = function(virtualNode, expectComponentValue) - return virtualNode.currentElement.component == expectComponentValue - end, - props = function(virtualNode, propSubSet) - local elementProps = virtualNode.currentElement.props - - for propKey, propValue in pairs(propSubSet) do - if elementProps[propKey] ~= propValue then - return false - end - end - - return true - end, - hostKey = function(virtualNode, expectHostKey) - return virtualNode.hostKey == expectHostKey - end, -} - local function countChildrenOfElement(element) if ElementKind.of(element) == ElementKind.Fragment then local count = 0 @@ -149,18 +122,15 @@ function ShallowWrapper:childrenCount() end function ShallowWrapper:find(constraints) - for constraint in pairs(constraints) do - if not ContraintFunctions[constraint] then - error(('unknown constraint %q'):format(constraint)) - end - end + VirtualNodeConstraints.validate(constraints) local results = {} local children = self:getChildren() for i=1, #children do local childWrapper = children[i] - if childWrapper:_satisfiesAllContraints(constraints) then + + if VirtualNodeConstraints.satisfiesAll(childWrapper._virtualNode, constraints) then table.insert(results, childWrapper) end end @@ -211,18 +181,4 @@ function ShallowWrapper:snapshotToString() return Snapshot.toString(self) end -function ShallowWrapper:_satisfiesAllContraints(constraints) - local virtualNode = self._virtualNode - - for constraint, value in pairs(constraints) do - local constraintFunction = ContraintFunctions[constraint] - - if not constraintFunction(virtualNode, value) then - return false - end - end - - return true -end - return ShallowWrapper \ No newline at end of file diff --git a/src/shallow/ShallowWrapper.spec.lua b/src/shallow/ShallowWrapper.spec.lua index 3df9755e..e2ddd7a5 100644 --- a/src/shallow/ShallowWrapper.spec.lua +++ b/src/shallow/ShallowWrapper.spec.lua @@ -427,315 +427,6 @@ return function() end) describe("find children", function() - local function Component(props) - return createElement("Frame", {}, props.children) - end - - describe("kind constraint", function() - it("should find the child element", function() - local childClassName = "TextLabel" - local element = createElement(Component, { - children = { - Child = createElement(childClassName), - }, - }) - - local result = shallow(element) - - local constraints = { - kind = ElementKind.Host, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Host) - expect(child.type.className).to.equal(childClassName) - end) - - it("should return an empty list when no children is found", function() - local element = createElement(Component, { - children = { - Child = createElement("TextLabel"), - }, - }) - - local result = shallow(element) - - local constraints = { - kind = ElementKind.Function, - } - local children = result:find(constraints) - - expect(next(children)).never.to.be.ok() - end) - end) - - describe("className constraint", function() - it("should find the child element", function() - local childClassName = "TextLabel" - local element = createElement(Component, { - children = { - Child = createElement(childClassName), - }, - }) - - local result = shallow(element) - - local constraints = { - className = childClassName, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Host) - expect(child.type.className).to.equal(childClassName) - end) - - it("should return an empty list when no children is found", function() - local element = createElement(Component, { - children = { - Child = createElement("TextLabel"), - }, - }) - - local result = shallow(element) - - local constraints = { - className = "Frame", - } - local children = result:find(constraints) - - expect(next(children)).never.to.be.ok() - end) - end) - - describe("component constraint", function() - it("should find the child element by it's class name", function() - local childClassName = "TextLabel" - local element = createElement(Component, { - children = { - Child = createElement(childClassName), - }, - }) - - local result = shallow(element) - - local constraints = { - component = childClassName, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Host) - expect(child.type.className).to.equal(childClassName) - end) - - it("should find the child element by it's function", function() - local function ChildComponent(props) - return nil - end - - local element = createElement(Component, { - children = { - Child = createElement(ChildComponent), - }, - }) - - local result = shallow(element) - - local constraints = { - component = ChildComponent, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Function) - expect(child.type.functionComponent).to.equal(ChildComponent) - end) - - it("should find the child element by it's component class", function() - local ChildComponent = RoactComponent:extend("ChildComponent") - - function ChildComponent:render() - return createElement("TextLabel") - end - - local element = createElement(Component, { - children = { - Child = createElement(ChildComponent), - }, - }) - - local result = shallow(element) - - local constraints = { - component = ChildComponent, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Stateful) - expect(child.type.component).to.equal(ChildComponent) - end) - - it("should return an empty list when no children is found", function() - local element = createElement(Component, { - children = { - Child = createElement("TextLabel"), - }, - }) - - local result = shallow(element) - - local constraints = { - component = "Frame", - } - local children = result:find(constraints) - - expect(next(children)).never.to.be.ok() - end) - end) - - describe("props constraint", function() - it("should find the child element that satisfies all prop constraints", function() - local childClassName = "Frame" - local props = { - Visible = false, - LayoutOrder = 7, - } - local element = createElement(Component, { - children = { - Child = createElement(childClassName, props), - }, - }) - - local result = shallow(element) - - local constraints = { - props = { - Visible = false, - LayoutOrder = 7, - }, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Host) - expect(child.type.className).to.equal(childClassName) - end) - - it("should find the child element from a subset of props", function() - local childClassName = "Frame" - local props = { - Visible = false, - LayoutOrder = 7, - } - local element = createElement(Component, { - children = { - Child = createElement(childClassName, props), - }, - }) - - local result = shallow(element) - - local constraints = { - props = { - LayoutOrder = 7, - }, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Host) - expect(child.type.className).to.equal(childClassName) - end) - - it("should return an empty list when no children is found", function() - local element = createElement(Component, { - children = { - Child = createElement("TextLabel", { - Visible = false, - LayoutOrder = 7, - }), - }, - }) - - local result = shallow(element) - - local constraints = { - props = { - Visible = false, - LayoutOrder = 4, - }, - } - local children = result:find(constraints) - - expect(next(children)).never.to.be.ok() - end) - end) - - describe("hostKey constraint", function() - it("should find the child element", function() - local hostKey = "Child" - local element = createElement(Component, { - children = { - [hostKey] = createElement("TextLabel"), - }, - }) - - local result = shallow(element) - - local constraints = { - hostKey = hostKey, - } - local children = result:find(constraints) - - expect(#children).to.equal(1) - - local child = children[1] - - expect(child.type.kind).to.equal(ElementKind.Host) - end) - - it("should return an empty list when no children is found", function() - local element = createElement(Component, { - children = { - Child = createElement("TextLabel"), - }, - }) - - local result = shallow(element) - - local constraints = { - hostKey = "NotFound", - } - local children = result:find(constraints) - - expect(next(children)).never.to.be.ok() - end) - end) - it("should throw if the constraint does not exist", function() local element = createElement("Frame") diff --git a/src/shallow/VirtualNodeConstraints/Constraints.lua b/src/shallow/VirtualNodeConstraints/Constraints.lua new file mode 100644 index 00000000..821b771e --- /dev/null +++ b/src/shallow/VirtualNodeConstraints/Constraints.lua @@ -0,0 +1,42 @@ +local RoactRoot = script.Parent.Parent.Parent + +local ElementKind = require(RoactRoot.ElementKind) + +local Constraints = setmetatable({}, { + __index = function(self, unexpectedConstraint) + error(("unknown constraint %q"):format(unexpectedConstraint)) + end, +}) + +function Constraints.kind(virtualNode, expectKind) + return ElementKind.of(virtualNode.currentElement) == expectKind +end + +function Constraints.className(virtualNode, className) + local element = virtualNode.currentElement + local isHost = ElementKind.of(element) == ElementKind.Host + + return isHost and element.component == className +end + +function Constraints.component(virtualNode, expectComponentValue) + return virtualNode.currentElement.component == expectComponentValue +end + +function Constraints.props(virtualNode, propSubSet) + local elementProps = virtualNode.currentElement.props + + for propKey, propValue in pairs(propSubSet) do + if elementProps[propKey] ~= propValue then + return false + end + end + + return true +end + +function Constraints.hostKey(virtualNode, expectHostKey) + return virtualNode.hostKey == expectHostKey +end + +return Constraints \ No newline at end of file diff --git a/src/shallow/VirtualNodeConstraints/Constraints.spec.lua b/src/shallow/VirtualNodeConstraints/Constraints.spec.lua new file mode 100644 index 00000000..5a40af42 --- /dev/null +++ b/src/shallow/VirtualNodeConstraints/Constraints.spec.lua @@ -0,0 +1,200 @@ +return function() + local RoactRoot = script.Parent.Parent.Parent + + local ElementKind = require(RoactRoot.ElementKind) + local createElement = require(RoactRoot.createElement) + local createReconciler = require(RoactRoot.createReconciler) + local RoactComponent = require(RoactRoot.Component) + local RobloxRenderer = require(RoactRoot.RobloxRenderer) + + local Constraints = require(script.Parent.Constraints) + + local robloxReconciler = createReconciler(RobloxRenderer) + + local HOST_PARENT = nil + local HOST_KEY = "ConstraintsTree" + + local function getVirtualNode(element) + return robloxReconciler.mountVirtualNode(element, HOST_PARENT, HOST_KEY) + end + + describe("kind", function() + it("should return true when the element is of the same kind", function() + local element = createElement("TextLabel") + local virtualNode = getVirtualNode(element) + + local result = Constraints.kind(virtualNode, ElementKind.Host) + + expect(result).to.equal(true) + end) + + it("should return false when the element is not of the same kind", function() + local element = createElement("TextLabel") + local virtualNode = getVirtualNode(element) + + local result = Constraints.kind(virtualNode, ElementKind.Stateful) + + expect(result).to.equal(false) + end) + end) + + describe("className", function() + it("should return true when a host virtualNode has the given class name", function() + local className = "TextLabel" + local element = createElement(className) + + local virtualNode = getVirtualNode(element) + + local result = Constraints.className(virtualNode, className) + + expect(result).to.equal(true) + end) + + it("should return false when a host virtualNode does not have the same class name", function() + local element = createElement("Frame") + + local virtualNode = getVirtualNode(element) + + local result = Constraints.className(virtualNode, "TextLabel") + + expect(result).to.equal(false) + end) + + it("should return false when not a host virtualNode", function() + local function Component() + return createElement("TextLabel") + end + local element = createElement(Component) + + local virtualNode = getVirtualNode(element) + + local result = Constraints.className(virtualNode, "TextLabel") + + expect(result).to.equal(false) + end) + end) + + describe("component", function() + it("should return true given a host virtualNode with the same class name", function() + local className = "TextLabel" + local element = createElement(className) + + local virtualNode = getVirtualNode(element) + + local result = Constraints.component(virtualNode, className) + + expect(result).to.equal(true) + end) + + it("should return true given a functional virtualNode function", function() + local function Component(props) + return nil + end + + local element = createElement(Component) + local virtualNode = getVirtualNode(element) + + local result = Constraints.component(virtualNode, Component) + + expect(result).to.equal(true) + end) + + it("should return true given a stateful virtualNode component class", function() + local Component = RoactComponent:extend("Foo") + + function Component:render() + return nil + end + + local element = createElement(Component) + local virtualNode = getVirtualNode(element) + + local result = Constraints.component(virtualNode, Component) + + expect(result).to.equal(true) + end) + + it("should return false when components kind do not match", function() + local function Component(props) + return nil + end + + local element = createElement(Component) + local virtualNode = getVirtualNode(element) + + local result = Constraints.component(virtualNode, "TextLabel") + + expect(result).to.equal(false) + end) + end) + + describe("props", function() + it("should return true when the virtualNode satisfies all prop constraints", function() + local props = { + Visible = false, + LayoutOrder = 7, + } + local element = createElement("TextLabel", props) + local virtualNode = getVirtualNode(element) + + local result = Constraints.props(virtualNode, { + Visible = false, + LayoutOrder = 7, + }) + + expect(result).to.equal(true) + end) + + it("should return true if the props are from a subset of the virtualNode props", function() + local props = { + Visible = false, + LayoutOrder = 7, + } + + local element = createElement("TextLabel", props) + local virtualNode = getVirtualNode(element) + + local result = Constraints.props(virtualNode, { + LayoutOrder = 7, + }) + + expect(result).to.equal(true) + end) + + it("should return false if a subset of the props are different from the given props", function() + local props = { + Visible = false, + LayoutOrder = 1, + } + + local element = createElement("TextLabel", props) + local virtualNode = getVirtualNode(element) + + local result = Constraints.props(virtualNode, { + LayoutOrder = 7, + }) + + expect(result).to.equal(false) + end) + end) + + describe("hostKey", function() + it("should return true when the virtualNode has the same hostKey", function() + local element = createElement("TextLabel") + local virtualNode = getVirtualNode(element) + + local result = Constraints.hostKey(virtualNode, HOST_KEY) + + expect(result).to.equal(true) + end) + + it("should return false when the virtualNode hostKey is different", function() + local element = createElement("TextLabel") + local virtualNode = getVirtualNode(element) + + local result = Constraints.hostKey(virtualNode, "foo") + + expect(result).to.equal(false) + end) + end) +end \ No newline at end of file diff --git a/src/shallow/VirtualNodeConstraints/init.lua b/src/shallow/VirtualNodeConstraints/init.lua new file mode 100644 index 00000000..1b3e2a7e --- /dev/null +++ b/src/shallow/VirtualNodeConstraints/init.lua @@ -0,0 +1,24 @@ +local Constraints = require(script.Constraints) + +local function satisfiesAll(virtualNode, constraints) + for constraint, value in pairs(constraints) do + local constraintFunction = Constraints[constraint] + + if not constraintFunction(virtualNode, value) then + return false + end + end + + return true +end + +local function validate(constraints) + for constraint in pairs(constraints) do + assert(Constraints[constraint] ~= nil, ("unknown constraint %q"):format(constraint)) + end +end + +return { + satisfiesAll = satisfiesAll, + validate = validate, +} \ No newline at end of file diff --git a/src/shallow/VirtualNodeConstraints/init.spec.lua b/src/shallow/VirtualNodeConstraints/init.spec.lua new file mode 100644 index 00000000..d7df1e91 --- /dev/null +++ b/src/shallow/VirtualNodeConstraints/init.spec.lua @@ -0,0 +1,31 @@ +return function() + local VirtualNodesConstraints = require(script.Parent) + + describe("validate", function() + it("should throw when a constraint does not exist", function() + local constraints = { + hostKey = "Key", + foo = "bar", + } + + local function validateNotExistingConstraint() + VirtualNodesConstraints.validate(constraints) + end + + expect(validateNotExistingConstraint).to.throw() + end) + + it("should not throw when all constraints exsits", function() + local constraints = { + hostKey = "Key", + className = "Frame", + } + + local function validateExistingConstraints() + VirtualNodesConstraints.validate(constraints) + end + + expect(validateExistingConstraints).never.to.throw() + end) + end) +end \ No newline at end of file From 90a0e40b53133775ab42f606b52fad9bdd5e5def Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 9 Aug 2019 10:11:41 -0700 Subject: [PATCH 31/65] Add VirtualTree --- src/Component.spec/setState.spec.lua | 25 +++++--- src/PureComponent.spec.lua | 13 ++++- src/VirtualTree.lua | 86 ++++++++++++++++++++++++++++ src/VirtualTree.spec.lua | 49 ++++++++++++++++ src/createReconciler.lua | 73 +---------------------- src/createReconciler.spec.lua | 20 ------- src/createReconcilerCompat.lua | 8 +-- src/createReconcilerCompat.spec.lua | 32 ++++++++--- src/init.lua | 21 ++++--- src/init.spec.lua | 1 - src/shallow/init.lua | 38 +++++++++--- src/shallow/init.spec.lua | 13 ++++- 12 files changed, 244 insertions(+), 135 deletions(-) create mode 100644 src/VirtualTree.lua create mode 100644 src/VirtualTree.spec.lua diff --git a/src/Component.spec/setState.spec.lua b/src/Component.spec/setState.spec.lua index 88ae9f5d..6526054c 100644 --- a/src/Component.spec/setState.spec.lua +++ b/src/Component.spec/setState.spec.lua @@ -4,11 +4,20 @@ return function() local createSpy = require(script.Parent.Parent.createSpy) local None = require(script.Parent.Parent.None) local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local VirtualTree = require(script.Parent.Parent.VirtualTree) local Component = require(script.Parent.Parent.Component) local noopReconciler = createReconciler(NoopRenderer) + local function mountWithNoop(element, hostParent, hostKey) + return VirtualTree.mount(element, { + hostParent = hostParent, + hostKey = hostKey, + reconciler = noopReconciler + }) + end + describe("setState", function() it("should not trigger an extra update when called in init", function() local renderCount = 0 @@ -35,7 +44,7 @@ return function() local initElement = createElement(InitComponent) - noopReconciler.mountVirtualTree(initElement) + mountWithNoop(initElement) expect(renderCount).to.equal(1) expect(updateCount).to.equal(0) @@ -53,7 +62,7 @@ return function() local renderElement = createElement(TestComponent) - local success, result = pcall(noopReconciler.mountVirtualTree, renderElement) + local success, result = pcall(mountWithNoop, renderElement) expect(success).to.equal(false) expect(result:match("render")).to.be.ok() @@ -76,9 +85,9 @@ return function() local initialElement = createElement(TestComponent) local updatedElement = createElement(TestComponent) - local tree = noopReconciler.mountVirtualTree(initialElement) + local tree = mountWithNoop(initialElement) - local success, result = pcall(noopReconciler.updateVirtualTree, tree, updatedElement) + local success, result = pcall(VirtualTree.update, tree, updatedElement) expect(success).to.equal(false) expect(result:match("shouldUpdate")).to.be.ok() @@ -100,9 +109,9 @@ return function() local initialElement = createElement(TestComponent) local updatedElement = createElement(TestComponent) - local tree = noopReconciler.mountVirtualTree(initialElement) + local tree = mountWithNoop(initialElement) - local success, result = pcall(noopReconciler.updateVirtualTree, tree, updatedElement) + local success, result = pcall(VirtualTree.update, tree, updatedElement) expect(success).to.equal(false) expect(result:match("willUpdate")).to.be.ok() @@ -123,9 +132,9 @@ return function() end local element = createElement(TestComponent) - local tree = noopReconciler.mountVirtualTree(element) + local tree = mountWithNoop(element) - local success, result = pcall(noopReconciler.unmountVirtualTree, tree) + local success, result = pcall(VirtualTree.unmount, tree) expect(success).to.equal(false) expect(result:match("willUnmount")).to.be.ok() diff --git a/src/PureComponent.spec.lua b/src/PureComponent.spec.lua index b1644373..31f7fc42 100644 --- a/src/PureComponent.spec.lua +++ b/src/PureComponent.spec.lua @@ -2,11 +2,20 @@ return function() local createElement = require(script.Parent.createElement) local NoopRenderer = require(script.Parent.NoopRenderer) local createReconciler = require(script.Parent.createReconciler) + local VirtualTree = require(script.Parent.VirtualTree) local PureComponent = require(script.Parent.PureComponent) local noopReconciler = createReconciler(NoopRenderer) + local function mountWithNoop(element, hostParent, hostKey) + return VirtualTree.mount(element, { + hostParent = hostParent, + hostKey = hostKey, + reconciler = noopReconciler + }) + end + it("should be extendable", function() local MyComponent = PureComponent:extend("MyComponent") @@ -50,7 +59,7 @@ return function() end local element = createElement(PureContainer) - local tree = noopReconciler.mountVirtualTree(element, nil, "PureComponent Tree") + local tree = mountWithNoop(element, nil, "PureComponent Tree") expect(updateCount).to.equal(0) @@ -70,6 +79,6 @@ return function() expect(updateCount).to.equal(3) - noopReconciler.unmountVirtualTree(tree) + VirtualTree.unmount(tree) end) end \ No newline at end of file diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua new file mode 100644 index 00000000..a788690c --- /dev/null +++ b/src/VirtualTree.lua @@ -0,0 +1,86 @@ +local createReconciler = require(script.Parent.createReconciler) +local RobloxRenderer = require(script.Parent.RobloxRenderer) +local shallow = require(script.Parent.shallow) +local Symbol = require(script.Parent.Symbol) +local Type = require(script.Parent.Type) + +local config = require(script.Parent.GlobalConfig).get() + +local DEFAULT_RENDERER = createReconciler(RobloxRenderer) + +local InternalData = Symbol.named("InternalData") + +local VirtualTree = {} +local VirtualTreePublic = {} +VirtualTreePublic.__index = VirtualTreePublic + +function VirtualTree.mount(element, options) + options = options or {} + local hostParent = options.hostParent + local hostKey = options.hostKey or "RoactTree" + local reconciler = options.reconciler or DEFAULT_RENDERER + + if config.typeChecks then + assert(Type.of(element) == Type.Element, "Expected arg #1 to be of type Element") + assert(reconciler.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + end + + local rootNode = reconciler.mountVirtualNode(element, hostParent, hostKey) + + local tree = { + [Type] = Type.VirtualTree, + [InternalData] = { + rootNode = rootNode, + mounted = true, + reconciler = reconciler, + }, + } + + setmetatable(tree, VirtualTreePublic) + + return tree +end + +function VirtualTree.update(tree, newElement) + local internalData = tree[InternalData] + + if config.typeChecks then + assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(Type.of(newElement) == Type.Element, "Expected arg #2 to be a Roact Element") + assert(internalData.mounted, "Cannot updated a Roact tree that has been unmounted") + end + + local reconciler = internalData.reconciler + + internalData.rootNode = reconciler.updateVirtualNode(internalData.rootNode, newElement) + + return tree +end + +function VirtualTree.unmount(tree) + local internalData = tree[InternalData] + + if config.typeChecks then + assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") + assert(internalData.mounted, "Cannot unmounted a Roact tree that has already been unmounted") + end + + internalData.mounted = false + + if internalData.rootNode ~= nil then + local reconciler = internalData.reconciler + + reconciler.unmountVirtualNode(internalData.rootNode) + end +end + +function VirtualTreePublic:getShallowWrapper(options) + assert(Type.of(self) == Type.VirtualTree, "Expected method getShallowWrapper to be called with `:`") + + local internalData = self[InternalData] + local rootNode = internalData.rootNode + + return shallow(rootNode, options) +end + +return VirtualTree \ No newline at end of file diff --git a/src/VirtualTree.spec.lua b/src/VirtualTree.spec.lua new file mode 100644 index 00000000..7037a929 --- /dev/null +++ b/src/VirtualTree.spec.lua @@ -0,0 +1,49 @@ +return function() + local createElement = require(script.Parent.createElement) + local createReconciler = require(script.Parent.createReconciler) + local NoopRenderer = require(script.Parent.NoopRenderer) + local VirtualTree = require(script.Parent.VirtualTree) + + local noopReconciler = createReconciler(NoopRenderer) + + local function mountWithNoop(element, hostParent, hostKey) + return VirtualTree.mount(element, { + hostParent = hostParent, + hostKey = hostKey, + reconciler = noopReconciler + }) + end + + describe("tree operations", function() + it("should mount and unmount", function() + local tree = mountWithNoop(createElement("StringValue")) + + expect(tree).to.be.ok() + + VirtualTree.unmount(tree) + end) + + it("should mount, update, and unmount", function() + local tree = mountWithNoop(createElement("StringValue")) + + expect(tree).to.be.ok() + + VirtualTree.update(tree, createElement("StringValue")) + + VirtualTree.unmount(tree) + end) + end) + + describe("getShallowWrapper", function() + it("should return a shallow wrapper", function() + local tree = VirtualTree.mount(createElement("StringValue")) + + expect(tree).to.be.ok() + + local wrapper = tree:getShallowWrapper() + + expect(wrapper).to.be.ok() + expect(wrapper.type.className).to.equal("StringValue") + end) + end) +end \ No newline at end of file diff --git a/src/createReconciler.lua b/src/createReconciler.lua index bcab1ba2..9055d275 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -2,13 +2,10 @@ local Type = require(script.Parent.Type) local ElementKind = require(script.Parent.ElementKind) local ElementUtils = require(script.Parent.ElementUtils) local Children = require(script.Parent.PropMarkers.Children) -local Symbol = require(script.Parent.Symbol) local internalAssert = require(script.Parent.internalAssert) local config = require(script.Parent.GlobalConfig).get() -local InternalData = Symbol.named("InternalData") - --[[ The reconciler is the mechanism in Roact that constructs the virtual tree that later gets turned into concrete objects by the renderer. @@ -345,78 +342,10 @@ local function createReconciler(renderer) return virtualNode end - --[[ - Constructs a new Roact virtual tree, constructs a root node for - it, and mounts it. - ]] - local function mountVirtualTree(element, hostParent, hostKey) - if config.typeChecks then - assert(Type.of(element) == Type.Element, "Expected arg #1 to be of type Element") - assert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") - end - - if hostKey == nil then - hostKey = "RoactTree" - end - - local tree = { - [Type] = Type.VirtualTree, - [InternalData] = { - -- The root node of the tree, which starts into the hierarchy of - -- Roact component instances. - rootNode = nil, - mounted = true, - }, - } - - tree[InternalData].rootNode = mountVirtualNode(element, hostParent, hostKey) - - return tree - end - - --[[ - Unmounts the virtual tree, freeing all of its resources. - - No further operations should be done on the tree after it's been - unmounted, as indicated by its the `mounted` field. - ]] - local function unmountVirtualTree(tree) - local internalData = tree[InternalData] - if config.typeChecks then - assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") - assert(internalData.mounted, "Cannot unmounted a Roact tree that has already been unmounted") - end - - internalData.mounted = false - - if internalData.rootNode ~= nil then - unmountVirtualNode(internalData.rootNode) - end - end - - --[[ - Utility method for updating the root node of a virtual tree given a new - element. - ]] - local function updateVirtualTree(tree, newElement) - local internalData = tree[InternalData] - if config.typeChecks then - assert(Type.of(tree) == Type.VirtualTree, "Expected arg #1 to be a Roact handle") - assert(Type.of(newElement) == Type.Element, "Expected arg #2 to be a Roact Element") - end - - internalData.rootNode = updateVirtualNode(internalData.rootNode, newElement) - - return tree - end - reconciler = { - mountVirtualTree = mountVirtualTree, - unmountVirtualTree = unmountVirtualTree, - updateVirtualTree = updateVirtualTree, - createVirtualNode = createVirtualNode, mountVirtualNode = mountVirtualNode, + isHostObject = renderer.isHostObject, unmountVirtualNode = unmountVirtualNode, updateVirtualNode = updateVirtualNode, updateVirtualNodeWithChildren = updateVirtualNodeWithChildren, diff --git a/src/createReconciler.spec.lua b/src/createReconciler.spec.lua index 193dd256..0739295f 100644 --- a/src/createReconciler.spec.lua +++ b/src/createReconciler.spec.lua @@ -11,26 +11,6 @@ return function() local noopReconciler = createReconciler(NoopRenderer) - describe("tree operations", function() - it("should mount and unmount", function() - local tree = noopReconciler.mountVirtualTree(createElement("StringValue")) - - expect(tree).to.be.ok() - - noopReconciler.unmountVirtualTree(tree) - end) - - it("should mount, update, and unmount", function() - local tree = noopReconciler.mountVirtualTree(createElement("StringValue")) - - expect(tree).to.be.ok() - - noopReconciler.updateVirtualTree(tree, createElement("StringValue")) - - noopReconciler.unmountVirtualTree(tree) - end) - end) - describe("booleans", function() it("should mount booleans as nil", function() local node = noopReconciler.mountVirtualNode(false, nil, "test") diff --git a/src/createReconcilerCompat.lua b/src/createReconcilerCompat.lua index e79cf5ac..b31865e8 100644 --- a/src/createReconcilerCompat.lua +++ b/src/createReconcilerCompat.lua @@ -20,25 +20,25 @@ Roact.reconcile has been renamed to Roact.update and will be removed in a future Check the call to Roact.reconcile at: ]] -local function createReconcilerCompat(reconciler) +local function createReconcilerCompat(virtualTree) local compat = {} function compat.reify(...) Logging.warnOnce(reifyMessage) - return reconciler.mountVirtualTree(...) + return virtualTree.mount(...) end function compat.teardown(...) Logging.warnOnce(teardownMessage) - return reconciler.unmountVirtualTree(...) + return virtualTree.unmount(...) end function compat.reconcile(...) Logging.warnOnce(reconcileMessage) - return reconciler.updateVirtualTree(...) + return virtualTree.update(...) end return compat diff --git a/src/createReconcilerCompat.spec.lua b/src/createReconcilerCompat.spec.lua index ea4d0789..33a9d156 100644 --- a/src/createReconcilerCompat.spec.lua +++ b/src/createReconcilerCompat.spec.lua @@ -3,11 +3,25 @@ return function() local createReconciler = require(script.Parent.createReconciler) local Logging = require(script.Parent.Logging) local NoopRenderer = require(script.Parent.NoopRenderer) + local VirtualTree = require(script.Parent.VirtualTree) local createReconcilerCompat = require(script.Parent.createReconcilerCompat) local noopReconciler = createReconciler(NoopRenderer) - local compatReconciler = createReconcilerCompat(noopReconciler) + + local function mountWithNoop(element, hostParent, hostKey) + return VirtualTree.mount(element, { + hostParent = hostParent, + hostKey = hostKey, + reconciler = noopReconciler + }) + end + + local compatReconciler = createReconcilerCompat({ + mount = mountWithNoop, + unmount = VirtualTree.unmount, + update = VirtualTree.update, + }) it("reify should only warn once per call site", function() local logInfo = Logging.capture(function() @@ -15,7 +29,7 @@ return function() -- warning hopefully. for _ = 1, 2 do local handle = compatReconciler.reify(createElement("StringValue")) - noopReconciler.unmountVirtualTree(handle) + VirtualTree.unmount(handle) end end) @@ -25,7 +39,7 @@ return function() logInfo = Logging.capture(function() -- This is a different call site, which should trigger another warning. local handle = compatReconciler.reify(createElement("StringValue")) - noopReconciler.unmountVirtualTree(handle) + VirtualTree.unmount(handle) end) expect(#logInfo.warnings).to.equal(1) @@ -37,7 +51,7 @@ return function() -- We're using a loop so that we get the same stack trace and only one -- warning hopefully. for _ = 1, 2 do - local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + local handle = mountWithNoop(createElement("StringValue")) compatReconciler.teardown(handle) end end) @@ -47,7 +61,7 @@ return function() logInfo = Logging.capture(function() -- This is a different call site, which should trigger another warning. - local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + local handle = mountWithNoop(createElement("StringValue")) compatReconciler.teardown(handle) end) @@ -60,9 +74,9 @@ return function() -- We're using a loop so that we get the same stack trace and only one -- warning hopefully. for _ = 1, 2 do - local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + local handle = mountWithNoop(createElement("StringValue")) compatReconciler.reconcile(handle, createElement("StringValue")) - noopReconciler.unmountVirtualTree(handle) + VirtualTree.unmount(handle) end end) @@ -71,9 +85,9 @@ return function() logInfo = Logging.capture(function() -- This is a different call site, which should trigger another warning. - local handle = noopReconciler.mountVirtualTree(createElement("StringValue")) + local handle = mountWithNoop(createElement("StringValue")) compatReconciler.reconcile(handle, createElement("StringValue")) - noopReconciler.unmountVirtualTree(handle) + VirtualTree.unmount(handle) end) expect(#logInfo.warnings).to.equal(1) diff --git a/src/init.lua b/src/init.lua index 8e9240f4..93e6ea4a 100644 --- a/src/init.lua +++ b/src/init.lua @@ -3,15 +3,19 @@ ]] local GlobalConfig = require(script.GlobalConfig) -local createReconciler = require(script.createReconciler) local createReconcilerCompat = require(script.createReconcilerCompat) -local RobloxRenderer = require(script.RobloxRenderer) local strict = require(script.strict) local Binding = require(script.Binding) -local shallow = require(script.shallow) +local VirtualTree = require(script.VirtualTree) -local robloxReconciler = createReconciler(RobloxRenderer) -local reconcilerCompat = createReconcilerCompat(robloxReconciler) +local function mount(element, hostParent, hostKey) + return VirtualTree.mount(element, { + hostParent = hostParent, + hostKey = hostKey, + }) +end + +local reconcilerCompat = createReconcilerCompat(VirtualTree) local Roact = strict { Component = require(script.Component), @@ -30,10 +34,9 @@ local Roact = strict { Event = require(script.PropMarkers.Event), Ref = require(script.PropMarkers.Ref), - mount = robloxReconciler.mountVirtualTree, - unmount = robloxReconciler.unmountVirtualTree, - update = robloxReconciler.updateVirtualTree, - shallow = shallow, + mount = mount, + unmount = VirtualTree.unmount, + update = VirtualTree.update, reify = reconcilerCompat.reify, teardown = reconcilerCompat.teardown, diff --git a/src/init.spec.lua b/src/init.spec.lua index 439620a6..7fcf79c8 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -11,7 +11,6 @@ return function() mount = "function", unmount = "function", update = "function", - shallow = "function", oneChild = "function", setGlobalConfig = "function", diff --git a/src/shallow/init.lua b/src/shallow/init.lua index e4dd6b9f..d589dc96 100644 --- a/src/shallow/init.lua +++ b/src/shallow/init.lua @@ -1,20 +1,42 @@ -local createReconciler = require(script.Parent.createReconciler) local Type = require(script.Parent.Type) -local RobloxRenderer = require(script.Parent.RobloxRenderer) local ShallowWrapper = require(script.ShallowWrapper) -local robloxReconciler = createReconciler(RobloxRenderer) +local optionsTypes = { + depth = "number", +} -local shallowTreeKey = "RoactTree" +local function validateOptions(options) + if options == nil then + return true + end -local function shallow(element, options) - assert(Type.of(element) == Type.Element, "Expected arg #1 to be an Element") + for key, value in pairs(options) do + local expectType = optionsTypes[key] + + if expectType == nil then + return false, ("unexpected option field %q (with value of %s)"):format( + tostring(key), + tostring(value) + ) + elseif typeof(value) ~= expectType then + return false, ("unexpected option type for %q (expected %s but got %s)"):format( + tostring(key), + expectType, + typeof(value) + ) + end + end + + return true +end + +local function shallow(rootNode, options) + assert(Type.of(rootNode) == Type.VirtualNode, "Expected arg #1 to be a VirtualNode") + assert(validateOptions(options)) options = options or {} local maxDepth = options.depth or 1 - local rootNode = robloxReconciler.mountVirtualNode(element, nil, shallowTreeKey) - return ShallowWrapper.new(rootNode, maxDepth) end diff --git a/src/shallow/init.spec.lua b/src/shallow/init.spec.lua index 10e0ee65..89c4a11d 100644 --- a/src/shallow/init.spec.lua +++ b/src/shallow/init.spec.lua @@ -1,7 +1,15 @@ return function() - local createElement = require(script.Parent.Parent.createElement) + local RoactRoot = script.Parent.Parent + + local createElement = require(RoactRoot.createElement) + local createReconciler = require(RoactRoot.createReconciler) + local RobloxRenderer = require(RoactRoot.RobloxRenderer) local shallow = require(script.Parent) + local robloxReconciler = createReconciler(RobloxRenderer) + + local shallowTreeKey = "RoactTree" + it("should return a shallow wrapper with depth = 1 by default", function() local element = createElement("Frame", {}, { Child = createElement("Frame", {}, { @@ -9,7 +17,8 @@ return function() }), }) - local wrapper = shallow(element) + local rootNode = robloxReconciler.mountVirtualNode(element, nil, shallowTreeKey) + local wrapper = shallow(rootNode) local childWrapper = wrapper:findUnique() expect(childWrapper:childrenCount()).to.equal(0) From 10c3041ce940c13890f55e8e7c23ed3c0cd4f34e Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 9 Aug 2019 10:13:04 -0700 Subject: [PATCH 32/65] Fix single quotes --- src/shallow/ShallowWrapper.lua | 2 +- src/shallow/Snapshot/init.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 2961f941..0db68880 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -31,7 +31,7 @@ local function getTypeFromVirtualNode(virtualNode) component = element.component, } else - error(('shallow wrapper does not support element of kind %q'):format(tostring(kind))) + error(("shallow wrapper does not support element of kind %q"):format(tostring(kind))) end end diff --git a/src/shallow/Snapshot/init.lua b/src/shallow/Snapshot/init.lua index 88b5ea84..4b9d631b 100644 --- a/src/shallow/Snapshot/init.lua +++ b/src/shallow/Snapshot/init.lua @@ -7,7 +7,7 @@ local invalidPattern = "[^" .. characterClass .. "]" local function createMatcher(identifier, shallowWrapper) if not identifier:match(identifierPattern) then - error(("Snapshot identifier has invalid character: '%s'"):format(identifier:match(invalidPattern))) + error(("Snapshot identifier has invalid character: %q"):format(identifier:match(invalidPattern))) end local snapshot = Serialize.wrapperToSnapshot(shallowWrapper) From 846ffd9a227a3bbc5bf3d5ce85ad28d9f6d47313 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 9 Aug 2019 11:00:37 -0700 Subject: [PATCH 33/65] Make the ShallowWrapper API strict --- src/shallow/ShallowWrapper.lua | 52 +++++++++++++++++++---------- src/shallow/ShallowWrapper.spec.lua | 18 +++++----- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 0db68880..e648cec1 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -3,12 +3,17 @@ local RoactRoot = script.Parent.Parent local Children = require(RoactRoot.PropMarkers.Children) local ElementKind = require(RoactRoot.ElementKind) local ElementUtils = require(RoactRoot.ElementUtils) -local VirtualNodeConstraints = require(script.Parent.VirtualNodeConstraints) +local strict = require(RoactRoot.strict) +local Symbol = require(RoactRoot.Symbol) local Snapshot = require(script.Parent.Snapshot) +local VirtualNodeConstraints = require(script.Parent.VirtualNodeConstraints) + +local InternalData = Symbol.named("InternalData") local ShallowWrapper = {} +local ShallowWrapperPublic = {} local ShallowWrapperMetatable = { - __index = ShallowWrapper, + __index = ShallowWrapperPublic, } local function getTypeFromVirtualNode(virtualNode) @@ -98,22 +103,25 @@ function ShallowWrapper.new(virtualNode, maxDepth) virtualNode = findNextVirtualNode(virtualNode, maxDepth) local wrapper = { - _virtualNode = virtualNode, - _childrenMaxDepth = maxDepth - 1, - _virtualNodeChildren = maxDepth == 0 and {} or virtualNode.children, + [InternalData] = { + virtualNode = virtualNode, + childrenMaxDepth = maxDepth - 1, + virtualNodeChildren = maxDepth == 0 and {} or virtualNode.children, + instance = virtualNode.hostObject, + }, type = getTypeFromVirtualNode(virtualNode), props = filterProps(virtualNode.currentElement.props), hostKey = virtualNode.hostKey, - instance = virtualNode.hostObject, } return setmetatable(wrapper, ShallowWrapperMetatable) end -function ShallowWrapper:childrenCount() +function ShallowWrapperPublic:childrenCount() local count = 0 + local internalData = self[InternalData] - for _, virtualNode in pairs(self._virtualNodeChildren) do + for _, virtualNode in pairs(internalData.virtualNodeChildren) do local element = virtualNode.currentElement count = count + countChildrenOfElement(element) end @@ -121,7 +129,7 @@ function ShallowWrapper:childrenCount() return count end -function ShallowWrapper:find(constraints) +function ShallowWrapperPublic:find(constraints) VirtualNodeConstraints.validate(constraints) local results = {} @@ -129,8 +137,9 @@ function ShallowWrapper:find(constraints) for i=1, #children do local childWrapper = children[i] + local childInternalData = childWrapper[InternalData] - if VirtualNodeConstraints.satisfiesAll(childWrapper._virtualNode, constraints) then + if VirtualNodeConstraints.satisfiesAll(childInternalData.virtualNode, constraints) then table.insert(results, childWrapper) end end @@ -138,7 +147,7 @@ function ShallowWrapper:find(constraints) return results end -function ShallowWrapper:findUnique(constraints) +function ShallowWrapperPublic:findUnique(constraints) local children = self:getChildren() if constraints == nil then @@ -159,17 +168,24 @@ function ShallowWrapper:findUnique(constraints) return constrainedChildren[1] end -function ShallowWrapper:getChildren() +function ShallowWrapperPublic:getChildren() local results = {} + local internalData = self[InternalData] - for _, childVirtualNode in pairs(self._virtualNodeChildren) do - getChildren(childVirtualNode, results, self._childrenMaxDepth) + for _, childVirtualNode in pairs(internalData.virtualNodeChildren) do + getChildren(childVirtualNode, results, internalData.childrenMaxDepth) end return results end -function ShallowWrapper:matchSnapshot(identifier) +function ShallowWrapperPublic:getInstance() + local internalData = self[InternalData] + + return internalData.instance +end + +function ShallowWrapperPublic:matchSnapshot(identifier) assert(typeof(identifier) == "string", "Snapshot identifier must be a string") local snapshotResult = Snapshot.createMatcher(identifier, self) @@ -177,8 +193,10 @@ function ShallowWrapper:matchSnapshot(identifier) snapshotResult:match() end -function ShallowWrapper:snapshotToString() +function ShallowWrapperPublic:snapshotToString() return Snapshot.toString(self) end -return ShallowWrapper \ No newline at end of file +strict(ShallowWrapperPublic, "ShallowWrapper") + +return strict(ShallowWrapper, "ShallowWrapper") \ No newline at end of file diff --git a/src/shallow/ShallowWrapper.spec.lua b/src/shallow/ShallowWrapper.spec.lua index e2ddd7a5..f217613c 100644 --- a/src/shallow/ShallowWrapper.spec.lua +++ b/src/shallow/ShallowWrapper.spec.lua @@ -395,22 +395,23 @@ return function() end) end) - describe("instance", function() - it("should contain the instance when it is a host component", function() + describe("getInstance", function() + it("should return the instance when it is a host component", function() local className = "Frame" local function Component(props) return createElement(className) end local element = createElement(Component) + local wrapper = shallow(element) - local result = shallow(element) + local instance = wrapper:getInstance() - expect(result.instance).to.be.ok() - expect(result.instance.ClassName).to.equal(className) + expect(instance).to.be.ok() + expect(instance.ClassName).to.equal(className) end) - it("should not have an instance if it is a function component", function() + it("should return nil if it is a function component", function() local function Child() return createElement("Frame") end @@ -419,10 +420,11 @@ return function() end local element = createElement(Component) + local wrapper = shallow(element) - local result = shallow(element) + local instance = wrapper:getInstance() - expect(result.instance).never.to.be.ok() + expect(instance).never.to.be.ok() end) end) From 764c00a003e833c493be8bc35b9a4fea28df5fc2 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 9 Aug 2019 14:27:57 -0700 Subject: [PATCH 34/65] Update API docs --- docs/api-reference.md | 154 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index a51216ca..dc654c3d 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -44,6 +44,22 @@ The result is a `RoactTree`, which is an opaque handle that represents a tree of --- +### Roact.shallow +``` +Roact.shallow(element, [options]) -> ShallowWrapper +``` +Options: +```lua +{ + depth: number -- default to 1 +} +``` + +Mounts the tree from the given element and wraps the root virtual node into a ShallowWrapper. +-- is this too specific/technical? + +--- + ### Roact.update ``` Roact.update(tree, element) -> RoactTree @@ -608,4 +624,140 @@ end As with `setState`, you can set use the constant `Roact.None` to remove a field from the state. !!! note - `getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. \ No newline at end of file + `getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. + +--- + +## ShallowWrapper + +### Fields + +#### type +``` +type: { + kind: ElementKind +} +``` + +The type dictionary always has the `kind` field that tell the component type. Additionally, depending of the kind of component, other information can be included. + +| kind | fields | description | +| --- | --- | --- | +| Host | className: string | the ClassName of the instance | +| Function | functionComponent: function | the function that renders the element | +| Stateful | component: table | the class-like table used to render the element | + +--- + +#### props +The props of the ShallowWrapper. + +!!! note + This dictionary will not contain the `Roact.Children` prop. To obtain the children elements wrapped into a ShallowWrapper, use the method `getChildren()` + +--- + +#### hostKey +The `hostKey` that is used to map the element to it's parent. + +--- + +### Methods + +#### childrenCount +``` +childrenCount() -> number +``` +Returns the amount of children that the current ShallowWrapper contains. + +--- + +#### find +``` +find([constraints]) -> list[ShallowWrapper] +``` +When a dictionary of constraints is provided, the function will filter the children that do not satisfy all given constraints. Otherwise, as `getChildren` do, it returns a list of all children wrapped into ShallowWrappers. + +--- + +#### findUnique +``` +findUnique([constraints]) -> ShallowWrapper +``` +Similar to `find`, this method will assert that only one child satisfies the given constraints, or in the case where none is provided, will assert that there is simply only one child. + +--- + +#### getChildren +``` +getChildren() -> list[ShallowWrapper] +``` +Returns a list of all children wrapped into ShallowWrappers. + +--- + +#### getInstance +``` +getInstance() -> Instance or nil +``` +Returns the instance object associated with the ShallowWrapper. It can return `nil` if the component wrapped by the ShallowWrapper does not render an instance, but rather another component. Here is an example: + +```lua +local function CoolComponent(props) + return Roact.createElement("TextLabel") +end + +local function MainComponent(props) + return Roact.createElement("Frame", {}, { + Cool = Roact.createElement(CoolComponent), + }) +end +``` + +If we shallow-render the `MainComponent`, the default depth will not render the CoolComponent. + +```lua +local element = Roact.createElement(MainComponent) + +local tree = Roact.mount(element) + +local wrapper = tree:getShallowWrapper() + +print(wrapper:getInstance() == nil) -- prints false + +local coolWrapper = wrapper:findUnique() + +print(coolWrapper:getInstance() == nil) -- prints true +``` + +--- + +#### matchSnapshot +``` +matchSnapshot(identifier) +``` +If no previous snapshot with the given identifier exists, it will create a new StringValue instance that will contain Lua code representing the current ShallowWrapper. When an existing snapshot is found (a ModuleScript named as the provided identifier), it will require the ModuleScript and load the data from it. Then, if the loaded data is different from the current ShallowWrapper, an error will be thrown. + +!!! note + As mentionned, `matchSnapshot` will create a StringValue, named like the given identifier, in which the generated lua code will be assigned to the Value property. When these values are generated in Studio during run mode, it's important to copy back the values and convert them into ModuleScripts. + +--- + +#### snapshotToString +``` +snapshotToString() -> string +``` +Returns the string source of the snapshot. Useful for debugging purposes. + +--- + +##### Constraints +Constraints are passed through a dictionary that maps a constraint name to it's value. + +| name | value type | description | +| --- | --- | --- | +| kind | ElementKind | Filters with the ElementKind of the rendered elements | +| className | string | Filters children that renders to host instance of the given class name | +| component | string, function or table | Filter children from their components, by finding the functional component original function, the sub-class table of Roact.Component or from the class name of the instance rendered | +| props | dictionary | Filters elements that contains at least the given set of props | +| hostKey | string | Filters elements by the host key used | From a36239ca3856c91ced9e9947f289dc68bb5dc3b4 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 9 Aug 2019 15:59:16 -0700 Subject: [PATCH 35/65] Add snapshots contributing guidelines --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd088337..2323671d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,6 +81,9 @@ When submitting a new feature, add tests for all functionality. We use [LuaCov](https://keplerproject.github.io/luacov) for keeping track of code coverage. We'd like it to be as close to 100% as possible, but it's not always possible. Adding tests just for the purpose of getting coverage isn't useful; we should strive to make only useful tests! +### Snapshots +As part of the test suite, there are tests that generates snapshot files (located under `./RoactSnapshots`). To sync back the generated StringValues produce by these on the file system, we suggest using a tool like [`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox) to make the workflow as simple as possible. In the case where you only need to sync a few files, you can use the plugin to automatically copy back the generated string values from the run mode into module scripts when going back to edit mode. Then simply copy-paste into the new snapshots into the file-system. + ## Release Checklist When releasing a new version of Roact, do these things: From 7ef84d3142fff2d52530eb9ce75d515339622b95 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 9 Aug 2019 16:10:05 -0700 Subject: [PATCH 36/65] Add plugin to keep snapshots from run to edit mode --- SnapshotsPlugin/EditModeMain.lua | 41 ++++++++++++++++++++++++++++++++ SnapshotsPlugin/RunModeMain.lua | 25 +++++++++++++++++++ SnapshotsPlugin/Settings.lua | 17 +++++++++++++ SnapshotsPlugin/init.server.lua | 12 ++++++++++ snapshots-plugin.project.json | 6 +++++ 5 files changed, 101 insertions(+) create mode 100644 SnapshotsPlugin/EditModeMain.lua create mode 100644 SnapshotsPlugin/RunModeMain.lua create mode 100644 SnapshotsPlugin/Settings.lua create mode 100644 SnapshotsPlugin/init.server.lua create mode 100644 snapshots-plugin.project.json diff --git a/SnapshotsPlugin/EditModeMain.lua b/SnapshotsPlugin/EditModeMain.lua new file mode 100644 index 00000000..eb8c3d90 --- /dev/null +++ b/SnapshotsPlugin/EditModeMain.lua @@ -0,0 +1,41 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Settings = require(script.Parent.Settings) + +local function SyncSnapshots(newSnapshots) + local snapshotsFolder = ReplicatedStorage:FindFirstChild(Settings.SnapshotFolderName) + + if not snapshotsFolder then + snapshotsFolder = Instance.new("Folder") + snapshotsFolder.Name = Settings.SnapshotFolderName + snapshotsFolder.Parent = ReplicatedStorage + end + + for name, value in pairs(newSnapshots) do + local snapshot = Instance.new("ModuleScript") + snapshot.Name = name + snapshot.Source = value + snapshot.Parent = snapshotsFolder + end +end + +local function PluginEditMode(plugin) + local isPluginDeactivated = false + + plugin.Deactivation:Connect(function() + isPluginDeactivated = true + end) + + while not isPluginDeactivated do + local newSnapshots = plugin:GetSetting(Settings.PluginSettingName) + + if newSnapshots then + SyncSnapshots(newSnapshots) + plugin:SetSetting(Settings.PluginSettingName, false) + end + + wait(Settings.SyncDelay) + end +end + +return PluginEditMode \ No newline at end of file diff --git a/SnapshotsPlugin/RunModeMain.lua b/SnapshotsPlugin/RunModeMain.lua new file mode 100644 index 00000000..53e8b161 --- /dev/null +++ b/SnapshotsPlugin/RunModeMain.lua @@ -0,0 +1,25 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Settings = require(script.Parent.Settings) + +local function PluginRunMode(plugin) + plugin.Unloading:Connect(function() + local snapshotsFolder = ReplicatedStorage:FindFirstChild(Settings.SnapshotFolderName) + + local newSnapshots = {} + + if not snapshotsFolder then + return + end + + for _, snapshot in pairs(snapshotsFolder:GetChildren()) do + if snapshot:IsA("StringValue") then + newSnapshots[snapshot.Name] = snapshot.Value + end + end + + plugin:SetSetting(Settings.PluginSettingName, newSnapshots) + end) +end + +return PluginRunMode \ No newline at end of file diff --git a/SnapshotsPlugin/Settings.lua b/SnapshotsPlugin/Settings.lua new file mode 100644 index 00000000..8e28f25a --- /dev/null +++ b/SnapshotsPlugin/Settings.lua @@ -0,0 +1,17 @@ +local function IndexError(_, key) + local message = ("%q (%s) is not a valid member of Settings"):format( + tostring(key), + typeof(key) + ) + + error(message, 2) +end + +return setmetatable({ + SnapshotFolderName = "RoactSnapshots", + PluginSettingName = "NewRoactSnapshots", + SyncDelay = 1, +}, { + __index = IndexError, + __newindex = IndexError, +}) \ No newline at end of file diff --git a/SnapshotsPlugin/init.server.lua b/SnapshotsPlugin/init.server.lua new file mode 100644 index 00000000..101f2da0 --- /dev/null +++ b/SnapshotsPlugin/init.server.lua @@ -0,0 +1,12 @@ +local RunService = game:GetService("RunService") + +local EditModeMain = require(script.EditModeMain) +local RunModeMain = require(script.RunModeMain) + +if RunService:IsEdit() then + EditModeMain(plugin) +else + if RunService:IsClient() then + RunModeMain(plugin) + end +end \ No newline at end of file diff --git a/snapshots-plugin.project.json b/snapshots-plugin.project.json new file mode 100644 index 00000000..843703c6 --- /dev/null +++ b/snapshots-plugin.project.json @@ -0,0 +1,6 @@ +{ + "name": "Snapshots Plugin", + "tree": { + "$path": "SnapshotsPlugin" + } + } \ No newline at end of file From dd5bc307e5c53e3a1c6c68bc9f1ff8458e680818 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 12 Aug 2019 13:40:25 -0700 Subject: [PATCH 37/65] Remove Roact.shallow from docs --- docs/api-reference.md | 45 +++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index dc654c3d..d5026c3f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -32,7 +32,7 @@ Creates a new Roact fragment with the provided table of elements. Fragments allo ### Roact.mount ``` -Roact.mount(element, [parent, [key]]) -> RoactTree +Roact.mount(element, [parent, [key]]) -> VirtualTree ``` !!! info @@ -40,29 +40,13 @@ Roact.mount(element, [parent, [key]]) -> RoactTree Creates a Roblox Instance given a Roact element, and optionally a `parent` to put it in, and a `key` to use as the instance's `Name`. -The result is a `RoactTree`, which is an opaque handle that represents a tree of components owned by Roact. You can pass this to APIs like `Roact.unmount`. It'll also be used for future debugging APIs. - ---- - -### Roact.shallow -``` -Roact.shallow(element, [options]) -> ShallowWrapper -``` -Options: -```lua -{ - depth: number -- default to 1 -} -``` - -Mounts the tree from the given element and wraps the root virtual node into a ShallowWrapper. --- is this too specific/technical? +The result is a `VirtualTree`, which is an opaque handle that represents a tree of components owned by Roact. You can pass this to APIs like `Roact.unmount`. It'll also be used for future debugging APIs. --- ### Roact.update ``` -Roact.update(tree, element) -> RoactTree +Roact.update(tree, element) -> VirtualTree ``` !!! info @@ -72,7 +56,7 @@ Updates an existing instance handle with a new element, returning a new handle. `update` can be used to change the props of a component instance created with `mount` and is useful for putting Roact content into non-Roact applications. -As of Roact 1.0, the returned `RoactTree` object will always be the same value as the one passed in. +As of Roact 1.0, the returned `VirtualTree` object will always be the same value as the one passed in. --- @@ -84,7 +68,7 @@ Roact.unmount(tree) -> void !!! info `Roact.unmount` is also available via the deprecated alias `Roact.teardown`. It will be removed in a future release. -Destroys the given `RoactTree` and all of its descendants. Does not operate on a Roblox Instance -- this must be given a handle that was returned by `Roact.mount`. +Destroys the given `VirtualTree` and all of its descendants. Does not operate on a Roblox Instance -- this must be given a handle that was returned by `Roact.mount`. --- @@ -761,3 +745,22 @@ Constraints are passed through a dictionary that maps a constraint name to it's | component | string, function or table | Filter children from their components, by finding the functional component original function, the sub-class table of Roact.Component or from the class name of the instance rendered | | props | dictionary | Filters elements that contains at least the given set of props | | hostKey | string | Filters elements by the host key used | + +--- + +## VirtualTree + +### Methods + +#### getShallowWrapper +``` +getShallowWrapper(options) -> ShallowWrapper +``` +Options: +```lua +{ + depth: number -- default to 1 +} +``` + +Wraps the current tree into a ShallowWrapper. From 3230ad43eb4e96f883faddfc594736e316bc6a61 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 12 Aug 2019 15:45:55 -0700 Subject: [PATCH 38/65] Add run-in-roblox docs to contributing guide --- CONTRIBUTING.md | 15 +++++++++ bin/run-tests-snapshots.lua | 26 +++++++++++++++ bin/sync_snapshots.lua | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 bin/run-tests-snapshots.lua create mode 100644 bin/sync_snapshots.lua diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2323671d..69edffeb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,6 +84,21 @@ We use [LuaCov](https://keplerproject.github.io/luacov) for keeping track of cod ### Snapshots As part of the test suite, there are tests that generates snapshot files (located under `./RoactSnapshots`). To sync back the generated StringValues produce by these on the file system, we suggest using a tool like [`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox) to make the workflow as simple as possible. In the case where you only need to sync a few files, you can use the plugin to automatically copy back the generated string values from the run mode into module scripts when going back to edit mode. Then simply copy-paste into the new snapshots into the file-system. +#### Using run-in-roblox +You can install run-in-roblox using [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) with the following command. +``` +cargo install run-in-roblox +``` +Once installed, run the [syncing script](bin/sync_snapshots.lua) to execute the tests and sync the created ModuleScripts from Roblox Studio to the file system. This script works by doing 3 tasks: +1. Build a test place with rojo +2. Use run-in-roblox to run [run-tests-snapshots.lua](bin/run-tests-snapshots.lua) in the place file +3. Parse the output of run-in-roblox to copy back the new snapshots to the file system + +Run this script using lua stand alone interpreter +``` +lua bin/run-tests-snapshots.lua +``` + ## Release Checklist When releasing a new version of Roact, do these things: diff --git a/bin/run-tests-snapshots.lua b/bin/run-tests-snapshots.lua new file mode 100644 index 00000000..73dfc898 --- /dev/null +++ b/bin/run-tests-snapshots.lua @@ -0,0 +1,26 @@ +-- luacheck: globals __LEMUR__ + +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local Roact = require(ReplicatedStorage.Roact) +local TestEZ = require(ReplicatedStorage.TestEZ) + +Roact.setGlobalConfig({ + ["internalTypeChecks"] = true, + ["typeChecks"] = true, + ["elementTracing"] = true, + ["propValidation"] = true, +}) +local results = TestEZ.TestBootstrap:run(ReplicatedStorage.Roact, TestEZ.Reporters.TextReporter) + +local RoactSnapshots = ReplicatedStorage:WaitForChild("RoactSnapshots", 1) + +if not RoactSnapshots then + return nil +end + +for _, snapshot in pairs(RoactSnapshots:GetChildren()) do + if snapshot:IsA("StringValue") then + print(("Snapshot:::<|%s|><|=>%s<=|>"):format(snapshot.Name, snapshot.Value)) + end +end \ No newline at end of file diff --git a/bin/sync_snapshots.lua b/bin/sync_snapshots.lua new file mode 100644 index 00000000..379b5435 --- /dev/null +++ b/bin/sync_snapshots.lua @@ -0,0 +1,64 @@ +local lfs = require("lfs") + +local ROJO_PROJECT_FILE = "place.project.json" +local TEST_FILE = "bin/run-tests-snapshots.lua" + +local SNAPSHOTS_FOLDER = "RoactSnapshots" +local TEST_PLACE_FILE_NAME = "temp-snapshot-place.rbxlx" + +local function writeSnapshotFile(name, content) + lfs.mkdir(SNAPSHOTS_FOLDER) + + local fileName = name .. ".lua" + local filePath = ("%s/%s"):format(SNAPSHOTS_FOLDER, fileName) + + print("Writing", filePath) + + local file = io.open(filePath, "w") + file:write(content) + file:close() +end + +local function executeCommand(command) + print(command) + local handle = io.popen(command) + local line = handle:read("*l") + local output = {line} + + while line do + line = handle:read("*l") + table.insert(output, line) + end + + handle:close() + + return table.concat(output, "\n") +end + +print("Building test place") +executeCommand(("rojo build %s -o %s"):format( + ROJO_PROJECT_FILE, + TEST_PLACE_FILE_NAME +)) + +print("Running run-in-roblox") +local output = executeCommand(("run-in-roblox %s -s %s -t 100"):format( + TEST_PLACE_FILE_NAME, + TEST_FILE +)) + +print("Clean test place") +os.remove(TEST_PLACE_FILE_NAME) + +print("Processing output...") + +local filteredOutput = output:gsub("\nSnapshot:::<|[%w_%-%.]+|><|=>.-<=|>", "") + +print(filteredOutput, "\n") + +for snapshotPattern in output:gmatch("Snapshot:::<|[%w_%-%.]+|><|=>.-<=|>") do + local name = snapshotPattern:match("Snapshot:::<|([%w_%-%.]+)|>") + local content = snapshotPattern:match("<|=>(.-)<=|>") + + writeSnapshotFile(name, content) +end \ No newline at end of file From 8986ce00fb3c5506782eb161f5545745a936f433 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 11:13:05 -0700 Subject: [PATCH 39/65] Move constraints next to find --- docs/api-reference.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index d5026c3f..4cf10602 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -664,6 +664,19 @@ When a dictionary of constraints is provided, the function will filter the child --- +##### Constraints +Constraints are passed through a dictionary that maps a constraint name to it's value. + +| name | value type | description | +| --- | --- | --- | +| kind | ElementKind | Filters with the ElementKind of the rendered elements | +| className | string | Filters children that renders to host instance of the given class name | +| component | string, function or table | Filter children from their components, by finding the functional component original function, the sub-class table of Roact.Component or from the class name of the instance rendered | +| props | dictionary | Filters elements that contains at least the given set of props | +| hostKey | string | Filters elements by the host key used | + +--- + #### findUnique ``` findUnique([constraints]) -> ShallowWrapper @@ -735,19 +748,6 @@ Returns the string source of the snapshot. Useful for debugging purposes. --- -##### Constraints -Constraints are passed through a dictionary that maps a constraint name to it's value. - -| name | value type | description | -| --- | --- | --- | -| kind | ElementKind | Filters with the ElementKind of the rendered elements | -| className | string | Filters children that renders to host instance of the given class name | -| component | string, function or table | Filter children from their components, by finding the functional component original function, the sub-class table of Roact.Component or from the class name of the instance rendered | -| props | dictionary | Filters elements that contains at least the given set of props | -| hostKey | string | Filters elements by the host key used | - ---- - ## VirtualTree ### Methods From 93249799f902684f0f686f3e4ef39847fdb982e0 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 11:14:36 -0700 Subject: [PATCH 40/65] Add shallow rendering page to docs (first pass) --- docs/advanced/shallow-rendering.md | 78 ++++++++++++++++ docs/scripts/sync-snapshots-with-lua.md | 102 +++++++++++++++++++++ docs/scripts/sync-snapshots-with-python.md | 94 +++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 275 insertions(+) create mode 100644 docs/advanced/shallow-rendering.md create mode 100644 docs/scripts/sync-snapshots-with-lua.md create mode 100644 docs/scripts/sync-snapshots-with-python.md diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md new file mode 100644 index 00000000..7168d4e2 --- /dev/null +++ b/docs/advanced/shallow-rendering.md @@ -0,0 +1,78 @@ +Shallow rendering is when you mount only a certain part an element tree. Technically, Roact does not provide the ability to shallow render a tree yet, but you can obtain a ShallowWrapper object that will mimic how shallow rendering works. + +## ShallowWrapper + +When writing tests for Roact components, you can mount your component using `Roact.mount` and then retrieve a ShallowWrapper object from the returned `VirtualTree`. + +```lua +-- let's assume there is a ComponentToTest that we want to test + +local virtualTree = Roact.mount(ComponentToTest) + +local shallowWrapper = tree:getShallowWrapper() +``` + +The ShallowWrapper is meant to provide an interface to help make assertions about behavior you expect from your component. + +--- + +## Snapshot Tests + +### What are Snapshots + +Snapshots are files that contains serialized data. In Roact's case, snapshots of ShallowWrapper objects can be generated. More specifically, the data contained from a ShallowWrapper is converted to Lua code, which is then saved into a ModuleScript. + +!!! Note + Currently, the generated snapshot will be stored in a StringValue. Often, the permission level where the test are ran does not make it possible to create a ModuleScript and assign it's Source property. For now, we rely on other tools like Roact's SnapshotsPlugin to copy the generated StringValue from Run mode to ModuleScript in Edit mode. + +During the tests execution, these snapshots are used to verify that they do not change through time. + +--- + +### What are Snapshot Tests + +The goal of snapshot tests is to make sure that the current serialized version of a snapshot matches the new generated one. This can be done through the `matchSnapshot` method on the ShallowWrapper. The string passed to the method will be to find the previous snapshot. + +```lua +shallowWrapper:matchSnapshot("ComponentToTest") +``` + +Here is a break down of what happen behind this method. + +1. Check if there is an existing snapshot with the given name. +2. If no snapshot exists, generate a new one from the ShallowWrapper and exit, otherwise continue +3. Require the ModuleScript (the snapshot) to obtain the table containing the data +4. Compare the loaded data with the generated data from the ShallowWrapper +5. Throw an error if the data is different from the loaded one + +--- + +### + +### Where Snapshot Tests Are Good + +Snapshot tests really shine when comes the time to test for regression. + +--- + +### Where Snapshot Tests Are Bad + +--- + +## Managing Snapshots + +### Within Roblox Studio + +When the tests are executed in Run mode (after Run is pressed), the snapshots are going to be created as StringValue objects inside a folder (ReplicatedStorage.RoactSnapshots). Then, pressing Stop to go back edit the place will delete all the new created snapshots values. In order to keep those values, a special plugin is needed to serialize the RoactSnapshots content into a plugin setting. Once the place go back in edit mode, the same plugin will detect the new content in the setting value and recreate the snapshots as ModuleScript objects. + + + +--- + +### On the File System + +Some users might be using a tool like `rojo` to sync files to Roblox Studio. To work with snapshots, something will be needed to sync the generated files from Roblox Studio to the file system. + +[`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox/) is [`Rust`](https://www.rust-lang.org/) project that allow Roblox to be run from the command line and sends the Roblox output content to the shell window. Using this tool, a script can be written to execute studio with a specific test runner that will print out the new snapshots in a special format. Then, the output can be parsed to find the new snapshots and write them to files. + +You can find these scripts written in Lua ([link](../scripts/sync-snapshots-with-lua.md)) or in python ([link](../scripts/sync-snapshots-with-python.md)) (compatible with version 2 and 3). These scripts will assume that you have the rojo and run-in-roblox commands available. They contain the same functionalities: it builds a place from a rojo configuration file, then it runs a specific script inside studio that should print the snapshots. The output is parsed to find the snapshots and write them. diff --git a/docs/scripts/sync-snapshots-with-lua.md b/docs/scripts/sync-snapshots-with-lua.md new file mode 100644 index 00000000..62a8119e --- /dev/null +++ b/docs/scripts/sync-snapshots-with-lua.md @@ -0,0 +1,102 @@ +Do not forget to adjust some of the variables to fit your project. + +!!! Warning + This script assume that `rojo` and `run-in-roblox` commands are available. Make sure that you have installed these two programs before executing this script. + + You will also need to modify the script that boot the tests run to print the new snapshots to the output. It can be done by adding these few lines after tests are run. + ```lua + --- add the following code after `TestEZ.TestBootstrap:run(...)` + local RoactSnapshots = ReplicatedStorage:WaitForChild("RoactSnapshots", 1) + + if not RoactSnapshots then + return nil + end + + for _, snapshot in pairs(RoactSnapshots:GetChildren()) do + if snapshot:IsA("StringValue") then + print(("Snapshot:::<|%s|><|=>%s<=|>"):format(snapshot.Name, snapshot.Value)) + end + end + ``` + +--- + +Using Lua stand alone interpreter, run the following script with the following command. +``` +lua path-to-file.lua +``` + +--- + +```lua +local lfs = require("lfs") + +-- the rojo configuration file used to build a roblox place where tests +-- are going to be run +local ROJO_PROJECT_FILE = "default.project.json" +-- the lua file that is going to be run inside the test place +-- this script needs to print the snapshots to the output +local TEST_FILE = "bin/print-snapshots.lua" + +-- the folder that contains the snapshots on the file system +local SNAPSHOTS_FOLDER = "RoactSnapshots" +-- the temporary file that will be created +local TEST_PLACE_FILE_NAME = "temp-snapshot-place.rbxlx" + +local function writeSnapshotFile(name, content) + lfs.mkdir(SNAPSHOTS_FOLDER) + + local fileName = name .. ".lua" + local filePath = ("%s/%s"):format(SNAPSHOTS_FOLDER, fileName) + + print("Writing", filePath) + + local file = io.open(filePath, "w") + file:write(content) + file:close() +end + +local function executeCommand(command) + print(command) + local handle = io.popen(command) + local line = handle:read("*l") + local output = {line} + + while line do + line = handle:read("*l") + table.insert(output, line) + end + + handle:close() + + return table.concat(output, "\n") +end + +print("Building test place") +executeCommand(("rojo build %s -o %s"):format( + ROJO_PROJECT_FILE, + TEST_PLACE_FILE_NAME +)) + +print("Running run-in-roblox") +local output = executeCommand(("run-in-roblox %s -s %s -t 60"):format( + TEST_PLACE_FILE_NAME, + TEST_FILE +)) + +print("Clean test place") +os.remove(TEST_PLACE_FILE_NAME) + +print("Processing output...") + +local filteredOutput = output:gsub("\nSnapshot:::<|[%w_%-%.]+|><|=>.-<=|>", "") + +print(filteredOutput, "\n") + +for snapshotPattern in output:gmatch("Snapshot:::<|[%w_%-%.]+|><|=>.-<=|>") do + local name = snapshotPattern:match("Snapshot:::<|([%w_%-%.]+)|>") + local content = snapshotPattern:match("<|=>(.-)<=|>") + + writeSnapshotFile(name, content) +end +``` \ No newline at end of file diff --git a/docs/scripts/sync-snapshots-with-python.md b/docs/scripts/sync-snapshots-with-python.md new file mode 100644 index 00000000..214afbe3 --- /dev/null +++ b/docs/scripts/sync-snapshots-with-python.md @@ -0,0 +1,94 @@ +Do not forget to adjust some of the variables to fit your project. + +!!! Warning + This script assume that `rojo` and `run-in-roblox` commands are available. Make sure that you have installed these two programs before executing this script. + + You will also need to modify the script that boot the tests run to print the new snapshots to the output. It can be done by adding these few lines after tests are run. + ```lua + --- add the following code after `TestEZ.TestBootstrap:run(...)` + local RoactSnapshots = ReplicatedStorage:WaitForChild("RoactSnapshots", 1) + + if not RoactSnapshots then + return nil + end + + for _, snapshot in pairs(RoactSnapshots:GetChildren()) do + if snapshot:IsA("StringValue") then + print(("Snapshot:::<|%s|><|=>%s<=|>"):format(snapshot.Name, snapshot.Value)) + end + end + ``` + +--- + +Using a python interpreter (compatible with 2 or 3), put the following script in a file and run it. +``` +python path-to-file.py +``` + +```python +import os +import re +import subprocess + +# the rojo configuration file used to build a roblox place where tests +# are going to be run +ROJO_PROJECT_FILE = 'place.project.json' +# the lua file that is going to be run inside the test place +# this script needs to print the snapshots to the output +TEST_FILE = 'bin/print-snapshots.lua' + +# the folder that contains the snapshots on the file system +SNAPSHOTS_FOLDER = 'RoactSnapshots' +# the temporary file that will be created +TEST_PLACE_FILE_NAME = 'temp-snapshot-place.rbxlx' + +PATTERN_STR = 'Snapshot:::<\|(?P[\w\.\-]+)\|><\|=>(?P.+?)<=\|>\n' + +pattern = re.compile(PATTERN_STR, re.MULTILINE | re.DOTALL) + + +def write_snapshot_file(name, content): + if not os.path.exists(SNAPSHOTS_FOLDER): + os.mkdir(SNAPSHOTS_FOLDER) + + file_name = name + '.lua' + file_path = os.path.join(SNAPSHOTS_FOLDER, file_name) + + print('Writing ' + file_path) + + with open(file_path, 'w') as snapshot_file: + snapshot_file.write(content) + + +def execute_command(command): + print(' '.join(command)) + return subprocess.check_output(command).decode('utf-8') + + +rojo_output = execute_command([ + 'rojo', + 'build', + ROJO_PROJECT_FILE, + '-o', + TEST_PLACE_FILE_NAME, +]) + +print(rojo_output) + +output = execute_command([ + 'run-in-roblox', + TEST_PLACE_FILE_NAME, + '-s', + TEST_FILE, + '-t', + '60', +]) + +print(pattern.sub('', output)) + +for match in pattern.finditer(output): + name = match.group('name') + content = match.group('content') + write_snapshot_file(name, content) +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 94430efe..0dc51fc7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,7 @@ nav: - Portals: advanced/portals.md - Bindings and Refs: advanced/bindings-and-refs.md - Context: advanced/context.md + - Shallow Rendering: advanced/shallow-rendering.md - Performance Optimization: - Overview: performance/overview.md - Reduce Reconcilation: performance/reduce-reconciliation.md From 5efa96ca292a0c8f6cfcebbe7d1bf9bd7de53c16 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 13:00:21 -0700 Subject: [PATCH 41/65] Restructure Shallow Rendering docs --- docs/advanced/shallow-rendering.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index 7168d4e2..060ae035 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -27,6 +27,9 @@ Snapshots are files that contains serialized data. In Roact's case, snapshots of During the tests execution, these snapshots are used to verify that they do not change through time. +!!! Note + For those using [luacheck](https://github.com/mpeterv/luacheck/) to analyse their Lua files, make sure to run the tool on the generated files. The format of the generated snapshots will probably fail luacheck (often just because of unused variables). There are no advantage to have these files match a specific format. + --- ### What are Snapshot Tests @@ -47,15 +50,13 @@ Here is a break down of what happen behind this method. --- -### - -### Where Snapshot Tests Are Good +#### Where They Are Good Snapshot tests really shine when comes the time to test for regression. --- -### Where Snapshot Tests Are Bad +#### Where They Are Bad --- @@ -65,8 +66,6 @@ Snapshot tests really shine when comes the time to test for regression. When the tests are executed in Run mode (after Run is pressed), the snapshots are going to be created as StringValue objects inside a folder (ReplicatedStorage.RoactSnapshots). Then, pressing Stop to go back edit the place will delete all the new created snapshots values. In order to keep those values, a special plugin is needed to serialize the RoactSnapshots content into a plugin setting. Once the place go back in edit mode, the same plugin will detect the new content in the setting value and recreate the snapshots as ModuleScript objects. - - --- ### On the File System From 342bbecd9a2d00e14c11ec46e27803b881cbb985 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 15:08:47 -0700 Subject: [PATCH 42/65] Improve snapshot workflow section --- docs/advanced/shallow-rendering.md | 102 ++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index 060ae035..a5f0d173 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -46,18 +46,118 @@ Here is a break down of what happen behind this method. 2. If no snapshot exists, generate a new one from the ShallowWrapper and exit, otherwise continue 3. Require the ModuleScript (the snapshot) to obtain the table containing the data 4. Compare the loaded data with the generated data from the ShallowWrapper -5. Throw an error if the data is different from the loaded one +5. Throw an error if the data is different from the loaded one and generate a new ModuleScript that contains the new generated snapshot (useful for comparison) + +--- + +#### Workflow Example + +For a concrete example, suppose the following component (probably in a script named `ComponentToTest`). + +```lua +local function ComponentToTest(props) + return Roact.createElement("TextLabel", { + Text = "foo", + }) +end +``` + +A snapshot test could be written this way (probably in a script named `ComponentToTest.spec`). + +```lua +it("should match the snapshot", function() + local element = Roact.createElement(ComponentToTest) + local tree = Roact.mount(element) + local shallowWrapper = tree:getShallowWrapper() + + shallowWrapper:matchSnapshot("ComponentToTest") +end) +``` + +After the first run, the test will have created a new script under `RoactSnapshots` called `ComponentToTest` that contains the following Lua code. + +```lua +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "TextLabel", + }, + hostKey = "RoactTree", + props = { + Text = "foo", + }, + children = {}, + } +end +``` + +Since these tests require the previous snapshots to compare with the current generated one, snapshots need to be committed to the version control software used for development. So the new component, the test and the generated snapshot would be commit and ready for review. The reviewer(s) will be able to review your snapshot as part of the normal review process. + +Suppose now ComponentToTest needs a change. We update it to the following snippet. + +```lua +local function ComponentToTest(props) + return Roact.createElement("TextLabel", { + Text = "bar", + }) +end +``` + +When we run back the previous test, it will fail and the message is going to tell us that the snapshots did not match. There will be a new script under `RoactSnapshots` called `ComponentToTest.NEW` that shows the new version of the snapshot. + +```lua +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "TextLabel", + }, + hostKey = "RoactTree", + props = { + Text = "bar", + }, + children = {}, + } +end +``` + +Since this example is trivial, it is easy to diff with human eyes and see that only the `Text` prop value changed from *foo* to *bar*. Since these changes are expected from the modification made to the component, we can delete the old snapshot and remove the `.NEW` from the newest one. If the tests are run again, they should all pass now. + +Again, the updated snapshot will be committed to source control along with the component changes. That way, the reviewer will see exactly what changed in the snapshot, so they can make sure the changes are expected. But why go through all this process for such a trivial change? + +Well, in most project complexity arise soon and components start to have more behavior. To make sure that certain behavior is not lost with a change, snapshot tests can assert that a button has a certain state after being clicked or while hovered. --- #### Where They Are Good +##### Regression + Snapshot tests really shine when comes the time to test for regression. +##### Carefully Reviewed + --- #### Where They Are Bad +##### Large Snapshots + +If a snapshot is created from a top level component with a ShallowWrapper that renders deeply, it can produce a really large snapshot file with lots of details. What is bad with this snapshot, is that everytime a child of this component will change, the snapshot will fail. + +This snapshot test will soon become an inconvenience and developers will slowly stop caring about it. The snapsot will not be reviewed correctly, because developers will be used to see the snapshot update on every new change submitted. + +To avoid this situation, it is truly important that each snapshots is kept as simple and small as possible. That is why the ShallowWrapper is deeply linked with the snapshot generation: it is needed to abstract the children of a component instead of making a snapshot that contains the whole tree. + --- ## Managing Snapshots From 750f6693127cc9422414de9b7c0eee1ca508f38d Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 15:57:44 -0700 Subject: [PATCH 43/65] Update snapshot review section --- docs/advanced/shallow-rendering.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index a5f0d173..f5a96f0b 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -146,6 +146,8 @@ Snapshot tests really shine when comes the time to test for regression. ##### Carefully Reviewed +Changes made to a snapshot file needs to be reviewed carefully as if it was hand written code. A reviewer needs to be able to catch any unexpected changes to a component. Any source control software should provide some way to see a diff of the changes that are going to be submitted. If a snapshot diff shows a difference of color property for a change that is supposed to update the sizing, the reviewer should point it to the developer and make sure the issue is solved because accepting the changes. + --- #### Where They Are Bad @@ -156,7 +158,7 @@ If a snapshot is created from a top level component with a ShallowWrapper that r This snapshot test will soon become an inconvenience and developers will slowly stop caring about it. The snapsot will not be reviewed correctly, because developers will be used to see the snapshot update on every new change submitted. -To avoid this situation, it is truly important that each snapshots is kept as simple and small as possible. That is why the ShallowWrapper is deeply linked with the snapshot generation: it is needed to abstract the children of a component instead of making a snapshot that contains the whole tree. +To avoid this situation, it is truly important that each snapshot is kept as simple and as small as possible. That is why the ShallowWrapper is deeply linked with the snapshot generation: it is needed to abstract the children of a component instead of making a snapshot that contains the whole tree. --- From 12539ecb0c74b6e62f77469a0523e5a6b229c22d Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 16:39:10 -0700 Subject: [PATCH 44/65] Rename ShallowWrapper method toSnapshotString --- docs/api-reference.md | 4 ++-- src/shallow/ShallowWrapper.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 4cf10602..b7106719 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -740,9 +740,9 @@ If no previous snapshot with the given identifier exists, it will create a new S --- -#### snapshotToString +#### toSnapshotString ``` -snapshotToString() -> string +toSnapshotString() -> string ``` Returns the string source of the snapshot. Useful for debugging purposes. diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index e648cec1..2f4a12d9 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -193,7 +193,7 @@ function ShallowWrapperPublic:matchSnapshot(identifier) snapshotResult:match() end -function ShallowWrapperPublic:snapshotToString() +function ShallowWrapperPublic:toSnapshotString() return Snapshot.toString(self) end From f1dac3773d60b1c3e6cebd73fb106e212cf91167 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 17:24:01 -0700 Subject: [PATCH 45/65] Remove kind constraint --- docs/api-reference.md | 1 - .../VirtualNodeConstraints/Constraints.lua | 4 ---- .../Constraints.spec.lua | 20 ------------------- 3 files changed, 25 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index b7106719..d8c83b30 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -669,7 +669,6 @@ Constraints are passed through a dictionary that maps a constraint name to it's | name | value type | description | | --- | --- | --- | -| kind | ElementKind | Filters with the ElementKind of the rendered elements | | className | string | Filters children that renders to host instance of the given class name | | component | string, function or table | Filter children from their components, by finding the functional component original function, the sub-class table of Roact.Component or from the class name of the instance rendered | | props | dictionary | Filters elements that contains at least the given set of props | diff --git a/src/shallow/VirtualNodeConstraints/Constraints.lua b/src/shallow/VirtualNodeConstraints/Constraints.lua index 821b771e..74d500b8 100644 --- a/src/shallow/VirtualNodeConstraints/Constraints.lua +++ b/src/shallow/VirtualNodeConstraints/Constraints.lua @@ -8,10 +8,6 @@ local Constraints = setmetatable({}, { end, }) -function Constraints.kind(virtualNode, expectKind) - return ElementKind.of(virtualNode.currentElement) == expectKind -end - function Constraints.className(virtualNode, className) local element = virtualNode.currentElement local isHost = ElementKind.of(element) == ElementKind.Host diff --git a/src/shallow/VirtualNodeConstraints/Constraints.spec.lua b/src/shallow/VirtualNodeConstraints/Constraints.spec.lua index 5a40af42..0b4f8b87 100644 --- a/src/shallow/VirtualNodeConstraints/Constraints.spec.lua +++ b/src/shallow/VirtualNodeConstraints/Constraints.spec.lua @@ -18,26 +18,6 @@ return function() return robloxReconciler.mountVirtualNode(element, HOST_PARENT, HOST_KEY) end - describe("kind", function() - it("should return true when the element is of the same kind", function() - local element = createElement("TextLabel") - local virtualNode = getVirtualNode(element) - - local result = Constraints.kind(virtualNode, ElementKind.Host) - - expect(result).to.equal(true) - end) - - it("should return false when the element is not of the same kind", function() - local element = createElement("TextLabel") - local virtualNode = getVirtualNode(element) - - local result = Constraints.kind(virtualNode, ElementKind.Stateful) - - expect(result).to.equal(false) - end) - end) - describe("className", function() it("should return true when a host virtualNode has the given class name", function() local className = "TextLabel" From cfd055a1135d67d435280acb91a7b199a981b18d Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 17:38:00 -0700 Subject: [PATCH 46/65] Improve coverage of validateShallowOptions --- src/shallow/init.lua | 32 ++---------------- src/shallow/validateShallowOptions.lua | 30 +++++++++++++++++ src/shallow/validateShallowOptions.spec.lua | 37 +++++++++++++++++++++ 3 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 src/shallow/validateShallowOptions.lua create mode 100644 src/shallow/validateShallowOptions.spec.lua diff --git a/src/shallow/init.lua b/src/shallow/init.lua index d589dc96..48386399 100644 --- a/src/shallow/init.lua +++ b/src/shallow/init.lua @@ -1,38 +1,10 @@ local Type = require(script.Parent.Type) local ShallowWrapper = require(script.ShallowWrapper) - -local optionsTypes = { - depth = "number", -} - -local function validateOptions(options) - if options == nil then - return true - end - - for key, value in pairs(options) do - local expectType = optionsTypes[key] - - if expectType == nil then - return false, ("unexpected option field %q (with value of %s)"):format( - tostring(key), - tostring(value) - ) - elseif typeof(value) ~= expectType then - return false, ("unexpected option type for %q (expected %s but got %s)"):format( - tostring(key), - expectType, - typeof(value) - ) - end - end - - return true -end +local validateShallowOptions = require(script.validateShallowOptions) local function shallow(rootNode, options) assert(Type.of(rootNode) == Type.VirtualNode, "Expected arg #1 to be a VirtualNode") - assert(validateOptions(options)) + assert(validateShallowOptions(options)) options = options or {} local maxDepth = options.depth or 1 diff --git a/src/shallow/validateShallowOptions.lua b/src/shallow/validateShallowOptions.lua new file mode 100644 index 00000000..eaecb9cf --- /dev/null +++ b/src/shallow/validateShallowOptions.lua @@ -0,0 +1,30 @@ +local optionsTypes = { + depth = "number", +} + +local function validateShallowOptions(options) + if options == nil then + return true + end + + for key, value in pairs(options) do + local expectType = optionsTypes[key] + + if expectType == nil then + return false, ("unexpected option field %q (with value of %s)"):format( + tostring(key), + tostring(value) + ) + elseif typeof(value) ~= expectType then + return false, ("unexpected option type for %q (expected %s but got %s)"):format( + tostring(key), + expectType, + typeof(value) + ) + end + end + + return true +end + +return validateShallowOptions \ No newline at end of file diff --git a/src/shallow/validateShallowOptions.spec.lua b/src/shallow/validateShallowOptions.spec.lua new file mode 100644 index 00000000..69670471 --- /dev/null +++ b/src/shallow/validateShallowOptions.spec.lua @@ -0,0 +1,37 @@ +return function() + local validateShallowOptions = require(script.Parent.validateShallowOptions) + + it("should return true given nil", function() + expect(validateShallowOptions(nil)).to.equal(true) + end) + + it("should return true given an empty table", function() + expect(validateShallowOptions({})).to.equal(true) + end) + + it("should return true if the key's value match the expected type", function() + local success = validateShallowOptions({ + depth = 1, + }) + + expect(success).to.equal(true) + end) + + it("should return false if a key is not expected", function() + local success, message = validateShallowOptions({ + foo = 1, + }) + + expect(success).to.equal(false) + expect(message).to.be.a("string") + end) + + it("should return false if an expected value has not the correct type", function() + local success, message = validateShallowOptions({ + depth = "foo", + }) + + expect(success).to.equal(false) + expect(message).to.be.a("string") + end) +end \ No newline at end of file From 4bb8234c423eb2bbb51f76fcb878c65bb6fd761b Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 14 Aug 2019 10:20:58 -0700 Subject: [PATCH 47/65] Remove unused require --- src/shallow/VirtualNodeConstraints/Constraints.spec.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shallow/VirtualNodeConstraints/Constraints.spec.lua b/src/shallow/VirtualNodeConstraints/Constraints.spec.lua index 0b4f8b87..86b6754d 100644 --- a/src/shallow/VirtualNodeConstraints/Constraints.spec.lua +++ b/src/shallow/VirtualNodeConstraints/Constraints.spec.lua @@ -1,7 +1,6 @@ return function() local RoactRoot = script.Parent.Parent.Parent - local ElementKind = require(RoactRoot.ElementKind) local createElement = require(RoactRoot.createElement) local createReconciler = require(RoactRoot.createReconciler) local RoactComponent = require(RoactRoot.Component) From 6d677c3121e68ca55f3b14932284b2a558f781d1 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Wed, 14 Aug 2019 10:29:48 -0700 Subject: [PATCH 48/65] Edit shallow rendering doc to use less second person and tighten up wording --- docs/advanced/shallow-rendering.md | 91 ++++++++++++++---------------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index f5a96f0b..14eb1c32 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -1,58 +1,50 @@ -Shallow rendering is when you mount only a certain part an element tree. Technically, Roact does not provide the ability to shallow render a tree yet, but you can obtain a ShallowWrapper object that will mimic how shallow rendering works. +To facilitate writing robust unit tests when using Roact, utilities are provided for shallow rendering and snapshotting components. Shallow rendering involves rendering 1 or more levels deep to assert facts about the rendered elements, allowing components to be tested as units. Snaphotting involves converting these shallow renders to serializable Lua modules for debugging purposes or to verify that components have not changed unexpectedly. -## ShallowWrapper +## Shallow Wrapper -When writing tests for Roact components, you can mount your component using `Roact.mount` and then retrieve a ShallowWrapper object from the returned `VirtualTree`. +When writing tests for Roact components, the `VirtualTree` returned by `Roact.mount` can provide a `ShallowWrapper` object with an interface designed to help make assertions about the expected behavior of a component. ```lua -- let's assume there is a ComponentToTest that we want to test -local virtualTree = Roact.mount(ComponentToTest) +local tree = Roact.mount(ComponentToTest) local shallowWrapper = tree:getShallowWrapper() ``` -The ShallowWrapper is meant to provide an interface to help make assertions about behavior you expect from your component. +### TODO: Testing Shallows Using Find +We should have a section on how and when to use find/findUnique ---- - -## Snapshot Tests +## Snapshots -### What are Snapshots +A snapshot is a serializable representation of a shallow rendered Roact tree that can be serialized as a complete Lua `ModuleScript`. The snapshot can either be compared with a past snapshot written to `ReplicatedStorage` or returned as a string for debugging purposes. -Snapshots are files that contains serialized data. In Roact's case, snapshots of ShallowWrapper objects can be generated. More specifically, the data contained from a ShallowWrapper is converted to Lua code, which is then saved into a ModuleScript. - -!!! Note - Currently, the generated snapshot will be stored in a StringValue. Often, the permission level where the test are ran does not make it possible to create a ModuleScript and assign it's Source property. For now, we rely on other tools like Roact's SnapshotsPlugin to copy the generated StringValue from Run mode to ModuleScript in Edit mode. +### Snapshot Testing -During the tests execution, these snapshots are used to verify that they do not change through time. - -!!! Note - For those using [luacheck](https://github.com/mpeterv/luacheck/) to analyse their Lua files, make sure to run the tool on the generated files. The format of the generated snapshots will probably fail luacheck (often just because of unused variables). There are no advantage to have these files match a specific format. - ---- - -### What are Snapshot Tests - -The goal of snapshot tests is to make sure that the current serialized version of a snapshot matches the new generated one. This can be done through the `matchSnapshot` method on the ShallowWrapper. The string passed to the method will be to find the previous snapshot. +The goal of a snapshot test is to verify that the shallow render matches the version saved previously. If a snapshot does not match, then the component may have changed unexpectedly. Snapshot testing can be done through the `matchSnapshot` method on the `ShallowWrapper`. The single argument of `matchSnapshot` is the name of the `ModuleScript` in `ReplicatedStorage` to look for. ```lua shallowWrapper:matchSnapshot("ComponentToTest") ``` -Here is a break down of what happen behind this method. +Here is a breakdown of how matching is performed: -1. Check if there is an existing snapshot with the given name. -2. If no snapshot exists, generate a new one from the ShallowWrapper and exit, otherwise continue -3. Require the ModuleScript (the snapshot) to obtain the table containing the data -4. Compare the loaded data with the generated data from the ShallowWrapper -5. Throw an error if the data is different from the loaded one and generate a new ModuleScript that contains the new generated snapshot (useful for comparison) +1. Check if there is an existing snapshot with the given name +2. If no snapshot exists, generate a new one from the `ShallowWrapper` and exit, else continue +3. Load the existing snapshot by calling require on the `ModuleScript` +4. Compare the existing snapshot with the one generated by the `ShallowWrapper` +5. If the snapshots match, exit, else save the new `ModuleScript` (useful for comparison) and throw an error +!!! Note + Currently, the `ModuleScript` for new snapshots will be stored in a `StringValue`. The permission level where the test are ran does not make it possible to create a `ModuleScript` and assign its `Source` property. Tools like Roact's SnapshotsPlugin can copy the `StringValue` from Run mode to a `ModuleScript` in Edit mode. + +!!! Note + The scripts generated by `matchSnapshot` are not guaranteed to pass [luacheck](https://github.com/mpeterv/luacheck/) due to the potential for unused variables, so be careful not to run `luacheck` on them. --- #### Workflow Example -For a concrete example, suppose the following component (probably in a script named `ComponentToTest`). +For a concrete example, suppose the following component `ComponentToTest`. ```lua local function ComponentToTest(props) @@ -62,7 +54,7 @@ local function ComponentToTest(props) end ``` -A snapshot test could be written this way (probably in a script named `ComponentToTest.spec`). +A snapshot test could be written this way: ```lua it("should match the snapshot", function() @@ -74,7 +66,7 @@ it("should match the snapshot", function() end) ``` -After the first run, the test will have created a new script under `RoactSnapshots` called `ComponentToTest` that contains the following Lua code. +After the first run, the test will have created a new script under `RoactSnapshots` in `ReplicatedStorage` called `ComponentToTest` that contains the following Lua code: ```lua return function(dependencies) @@ -96,9 +88,9 @@ return function(dependencies) end ``` -Since these tests require the previous snapshots to compare with the current generated one, snapshots need to be committed to the version control software used for development. So the new component, the test and the generated snapshot would be commit and ready for review. The reviewer(s) will be able to review your snapshot as part of the normal review process. +Since these tests require the previous snapshots to compare with the current generated one, snapshots should be saved (if using Studio) or committed to version control (if using file system development). -Suppose now ComponentToTest needs a change. We update it to the following snippet. +Suppose now `ComponentToTest` is updated as follows: ```lua local function ComponentToTest(props) @@ -108,7 +100,7 @@ local function ComponentToTest(props) end ``` -When we run back the previous test, it will fail and the message is going to tell us that the snapshots did not match. There will be a new script under `RoactSnapshots` called `ComponentToTest.NEW` that shows the new version of the snapshot. +When the test is run again, it will fail, noting that the snapshots did not match. There will be a new script under `RoactSnapshots` called `ComponentToTest.NEW` that shows the new version of the snapshot. ```lua return function(dependencies) @@ -130,11 +122,11 @@ return function(dependencies) end ``` -Since this example is trivial, it is easy to diff with human eyes and see that only the `Text` prop value changed from *foo* to *bar*. Since these changes are expected from the modification made to the component, we can delete the old snapshot and remove the `.NEW` from the newest one. If the tests are run again, they should all pass now. +Only the `Text` prop value changed, from *foo* to *bar*. Since these changes are expected from the modification made to the component, we can delete the old snapshot and remove the `.NEW` from the newest one. If the tests are run again, they will once again pass. -Again, the updated snapshot will be committed to source control along with the component changes. That way, the reviewer will see exactly what changed in the snapshot, so they can make sure the changes are expected. But why go through all this process for such a trivial change? +Updated snapshots should be saved / committed along with the component changes to make it clear why the snapshot is being changed. -Well, in most project complexity arise soon and components start to have more behavior. To make sure that certain behavior is not lost with a change, snapshot tests can assert that a button has a certain state after being clicked or while hovered. +Most snapshots will be more complex than this example and act as a powerful line of defense against unexpected changes to components. --- @@ -146,7 +138,7 @@ Snapshot tests really shine when comes the time to test for regression. ##### Carefully Reviewed -Changes made to a snapshot file needs to be reviewed carefully as if it was hand written code. A reviewer needs to be able to catch any unexpected changes to a component. Any source control software should provide some way to see a diff of the changes that are going to be submitted. If a snapshot diff shows a difference of color property for a change that is supposed to update the sizing, the reviewer should point it to the developer and make sure the issue is solved because accepting the changes. +Changes made to a snapshot file needs to be reviewed carefully as if it was hand written code. A reviewer needs to be able to catch any unexpected changes to a component. Any source control software should provide some way to see a diff of the changes that are going to be submitted. If a snapshot diff shows a difference in the color property for a change that is supposed to update sizing, the reviewer should verify that the change is intended. --- @@ -154,26 +146,27 @@ Changes made to a snapshot file needs to be reviewed carefully as if it was hand ##### Large Snapshots -If a snapshot is created from a top level component with a ShallowWrapper that renders deeply, it can produce a really large snapshot file with lots of details. What is bad with this snapshot, is that everytime a child of this component will change, the snapshot will fail. +If a snapshot is created from a top level component with a ShallowWrapper that renders many levels deep, it can produce a large snapshot file with potentially hundreds of lines. The larger the snapshot, the more likely it is to fail due to a reason unrelated to the component being tested. -This snapshot test will soon become an inconvenience and developers will slowly stop caring about it. The snapsot will not be reviewed correctly, because developers will be used to see the snapshot update on every new change submitted. +Large snapshots also become an inconvenience and may not be reviewed correctly due to their size or frequency of needing an update, or both. -To avoid this situation, it is truly important that each snapshot is kept as simple and as small as possible. That is why the ShallowWrapper is deeply linked with the snapshot generation: it is needed to abstract the children of a component instead of making a snapshot that contains the whole tree. +To avoid this situation, it is important that each snapshot is kept as simple and as small as possible. That is why the `ShallowWrapper` is deeply linked with snapshot generation and defaults to rendering only 1 level deep. Render more deeply only as needed to exercise the component being tested. --- -## Managing Snapshots +#### Managing Snapshots -### Within Roblox Studio +When the tests are executed in Run mode (after Run is pressed), snapshots are serialized and saved as `StringValue` objects inside of `ReplicatedStorage.RoactSnapshots`. Pressing Stop to go back to Edit mode will delete any newly created snapshots values. Preserving the serialized snapshots and saving them as module scripts is necessary to ensure that there are snapshots to match with during future test runs. The method of preserving them varies based on the development environment being used. -When the tests are executed in Run mode (after Run is pressed), the snapshots are going to be created as StringValue objects inside a folder (ReplicatedStorage.RoactSnapshots). Then, pressing Stop to go back edit the place will delete all the new created snapshots values. In order to keep those values, a special plugin is needed to serialize the RoactSnapshots content into a plugin setting. Once the place go back in edit mode, the same plugin will detect the new content in the setting value and recreate the snapshots as ModuleScript objects. +##### Roblox Studio + If using Roblox Studio for development, install the `RoactSnapshots` plugin, which will preserve the `StringValue` objects and save them as `ModuleScript` objects upon returning to Edit mode. --- -### On the File System +##### File System -Some users might be using a tool like `rojo` to sync files to Roblox Studio. To work with snapshots, something will be needed to sync the generated files from Roblox Studio to the file system. +If using a tool like `rojo` to sync files to Roblox Studio, a tool like `run-in-roblox` can help write the module scripts back to the file system. -[`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox/) is [`Rust`](https://www.rust-lang.org/) project that allow Roblox to be run from the command line and sends the Roblox output content to the shell window. Using this tool, a script can be written to execute studio with a specific test runner that will print out the new snapshots in a special format. Then, the output can be parsed to find the new snapshots and write them to files. +[`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox/) is a [`Rust`](https://www.rust-lang.org/) command line tool that runs Roblox Studio and sends content from the output to the shell window. Using this tool, a script can be written to open a place file and run it with a specific test runner that can print out the new snapshots in a special format. Then, the output can be parsed to find the new snapshots and write them to files. -You can find these scripts written in Lua ([link](../scripts/sync-snapshots-with-lua.md)) or in python ([link](../scripts/sync-snapshots-with-python.md)) (compatible with version 2 and 3). These scripts will assume that you have the rojo and run-in-roblox commands available. They contain the same functionalities: it builds a place from a rojo configuration file, then it runs a specific script inside studio that should print the snapshots. The output is parsed to find the snapshots and write them. +Here are examples of this kind of script written in Lua ([link](../scripts/sync-snapshots-with-lua.md)) and in python ([link](../scripts/sync-snapshots-with-python.md)) (compatible with version 2 and 3). These example scripts assume that the `rojo` and `run-in-roblox` commands are available. They build a place from a `rojo` configuration file, run a specific script inside studio, print the serialized snapshots, parse them from the output, and write them to the file system. From 7073e0d3bc36cfcf19ad517d3f15759743ddfc94 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 14 Aug 2019 11:32:15 -0700 Subject: [PATCH 49/65] Remove ShallowWrapper:getChildren --- docs/api-reference.md | 16 +++---- src/shallow/ShallowWrapper.lua | 53 ++++++++------------- src/shallow/Snapshot/Serialize/Snapshot.lua | 2 +- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index d8c83b30..0b5e449b 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -633,11 +633,16 @@ The type dictionary always has the `kind` field that tell the component type. Ad --- +#### children +The list of children components wrapped into ShallowWrappers. + +--- + #### props The props of the ShallowWrapper. !!! note - This dictionary will not contain the `Roact.Children` prop. To obtain the children elements wrapped into a ShallowWrapper, use the method `getChildren()` + This dictionary will not contain the `Roact.Children` prop. To obtain the children elements wrapped into a ShallowWrapper, use the `children` field. --- @@ -660,7 +665,7 @@ Returns the amount of children that the current ShallowWrapper contains. ``` find([constraints]) -> list[ShallowWrapper] ``` -When a dictionary of constraints is provided, the function will filter the children that do not satisfy all given constraints. Otherwise, as `getChildren` do, it returns a list of all children wrapped into ShallowWrappers. +When a dictionary of constraints is provided, the function will filter the children that do not satisfy all given constraints. Otherwise, it returns the `children` field that contains all children wrapped into ShallowWrappers. --- @@ -684,13 +689,6 @@ Similar to `find`, this method will assert that only one child satisfies the giv --- -#### getChildren -``` -getChildren() -> list[ShallowWrapper] -``` -Returns a list of all children wrapped into ShallowWrappers. - ---- #### getInstance ``` diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 2f4a12d9..1b4eaf47 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -102,41 +102,39 @@ end function ShallowWrapper.new(virtualNode, maxDepth) virtualNode = findNextVirtualNode(virtualNode, maxDepth) + local internalData = { + virtualNode = virtualNode, + childrenMaxDepth = maxDepth - 1, + virtualNodeChildren = maxDepth == 0 and {} or virtualNode.children, + instance = virtualNode.hostObject, + } + local wrapper = { - [InternalData] = { - virtualNode = virtualNode, - childrenMaxDepth = maxDepth - 1, - virtualNodeChildren = maxDepth == 0 and {} or virtualNode.children, - instance = virtualNode.hostObject, - }, + [InternalData] = internalData, type = getTypeFromVirtualNode(virtualNode), props = filterProps(virtualNode.currentElement.props), hostKey = virtualNode.hostKey, + children = {}, } + for _, childVirtualNode in pairs(internalData.virtualNodeChildren) do + getChildren(childVirtualNode, wrapper.children, internalData.childrenMaxDepth) + end + return setmetatable(wrapper, ShallowWrapperMetatable) end function ShallowWrapperPublic:childrenCount() - local count = 0 - local internalData = self[InternalData] - - for _, virtualNode in pairs(internalData.virtualNodeChildren) do - local element = virtualNode.currentElement - count = count + countChildrenOfElement(element) - end - - return count + return #self.children end function ShallowWrapperPublic:find(constraints) VirtualNodeConstraints.validate(constraints) local results = {} - local children = self:getChildren() - for i=1, #children do - local childWrapper = children[i] + for i=1, #self.children do + local childWrapper = self.children[i] local childInternalData = childWrapper[InternalData] if VirtualNodeConstraints.satisfiesAll(childInternalData.virtualNode, constraints) then @@ -148,14 +146,12 @@ function ShallowWrapperPublic:find(constraints) end function ShallowWrapperPublic:findUnique(constraints) - local children = self:getChildren() - if constraints == nil then assert( - #children == 1, - ("expect to contain exactly one child, but found %d"):format(#children) + #self.children == 1, + ("expect to contain exactly one child, but found %d"):format(#self.children) ) - return children[1] + return self.children[1] end local constrainedChildren = self:find(constraints) @@ -168,17 +164,6 @@ function ShallowWrapperPublic:findUnique(constraints) return constrainedChildren[1] end -function ShallowWrapperPublic:getChildren() - local results = {} - local internalData = self[InternalData] - - for _, childVirtualNode in pairs(internalData.virtualNodeChildren) do - getChildren(childVirtualNode, results, internalData.childrenMaxDepth) - end - - return results -end - function ShallowWrapperPublic:getInstance() local internalData = self[InternalData] diff --git a/src/shallow/Snapshot/Serialize/Snapshot.lua b/src/shallow/Snapshot/Serialize/Snapshot.lua index 137934e6..f55c87cc 100644 --- a/src/shallow/Snapshot/Serialize/Snapshot.lua +++ b/src/shallow/Snapshot/Serialize/Snapshot.lua @@ -117,7 +117,7 @@ function Snapshot.new(wrapper) type = Snapshot.type(wrapper.type), hostKey = wrapper.hostKey, props = Snapshot.props(wrapper.props), - children = Snapshot.children(wrapper:getChildren()), + children = Snapshot.children(wrapper.children), } end From 2764cbc20b7532115a2b91b480445253c8274e95 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 14 Aug 2019 13:28:42 -0700 Subject: [PATCH 50/65] Remove ShallowWrapper:getChildrenCount() --- docs/api-reference.md | 8 -------- src/shallow/ShallowWrapper.lua | 18 ------------------ src/shallow/ShallowWrapper.spec.lua | 22 +++++++++++----------- src/shallow/init.spec.lua | 2 +- 4 files changed, 12 insertions(+), 38 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 0b5e449b..92db3107 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -653,14 +653,6 @@ The `hostKey` that is used to map the element to it's parent. ### Methods -#### childrenCount -``` -childrenCount() -> number -``` -Returns the amount of children that the current ShallowWrapper contains. - ---- - #### find ``` find([constraints]) -> list[ShallowWrapper] diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 1b4eaf47..85df47b7 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -54,20 +54,6 @@ local function findNextVirtualNode(virtualNode, maxDepth) return currentNode end -local function countChildrenOfElement(element) - if ElementKind.of(element) == ElementKind.Fragment then - local count = 0 - - for _, subElement in pairs(element.elements) do - count = count + countChildrenOfElement(subElement) - end - - return count - else - return 1 - end -end - local function getChildren(virtualNode, results, maxDepth) if ElementKind.of(virtualNode.currentElement) == ElementKind.Fragment then for _, subVirtualNode in pairs(virtualNode.children) do @@ -124,10 +110,6 @@ function ShallowWrapper.new(virtualNode, maxDepth) return setmetatable(wrapper, ShallowWrapperMetatable) end -function ShallowWrapperPublic:childrenCount() - return #self.children -end - function ShallowWrapperPublic:find(constraints) VirtualNodeConstraints.validate(constraints) diff --git a/src/shallow/ShallowWrapper.spec.lua b/src/shallow/ShallowWrapper.spec.lua index f217613c..e0d06866 100644 --- a/src/shallow/ShallowWrapper.spec.lua +++ b/src/shallow/ShallowWrapper.spec.lua @@ -52,7 +52,7 @@ return function() local result = shallow(element) - expect(result:childrenCount()).to.equal(0) + expect(#result.children).to.equal(0) end) end) @@ -207,7 +207,7 @@ return function() depth = 0, }) - expect(result:childrenCount()).to.equal(0) + expect(#result.children).to.equal(0) end) it("should not include any grand-children when depth is one", function() @@ -223,18 +223,18 @@ return function() depth = 1, }) - expect(result:childrenCount()).to.equal(1) + expect(#result.children).to.equal(1) local componentWithChildrenWrapper = result:find({ component = ComponentWithChildren, })[1] expect(componentWithChildrenWrapper).to.be.ok() - expect(componentWithChildrenWrapper:childrenCount()).to.equal(0) + expect(#componentWithChildrenWrapper.children).to.equal(0) end) end) - describe("childrenCount", function() + describe("children count", function() local childClassName = "TextLabel" local function Component(props) @@ -247,24 +247,24 @@ return function() return createElement("Frame", {}, children) end - it("should return 1 when the element contains only one child element", function() + it("should have 1 child when the element contains only one element", function() local element = createElement(Component, { childrenCount = 1, }) local result = shallow(element) - expect(result:childrenCount()).to.equal(1) + expect(#result.children).to.equal(1) end) - it("should return 0 when the element does not contain elements", function() + it("should not have any children when the element does not contain elements", function() local element = createElement(Component, { childrenCount = 0, }) local result = shallow(element) - expect(result:childrenCount()).to.equal(0) + expect(#result.children).to.equal(0) end) it("should count children in a fragment", function() @@ -277,7 +277,7 @@ return function() local result = shallow(element) - expect(result:childrenCount()).to.equal(2) + expect(#result.children).to.equal(2) end) it("should count children nested in fragments", function() @@ -293,7 +293,7 @@ return function() local result = shallow(element) - expect(result:childrenCount()).to.equal(3) + expect(#result.children).to.equal(3) end) end) diff --git a/src/shallow/init.spec.lua b/src/shallow/init.spec.lua index 89c4a11d..8e46bc0b 100644 --- a/src/shallow/init.spec.lua +++ b/src/shallow/init.spec.lua @@ -21,6 +21,6 @@ return function() local wrapper = shallow(rootNode) local childWrapper = wrapper:findUnique() - expect(childWrapper:childrenCount()).to.equal(0) + expect(#childWrapper.children).to.equal(0) end) end \ No newline at end of file From 3f44eabfb8fd1483e66feac6b35dda0e14b9d03b Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 15 Aug 2019 12:47:08 -0700 Subject: [PATCH 51/65] Rename ShallowWrapper getInstance to getHostObject --- docs/api-reference.md | 8 ++++---- src/shallow/ShallowWrapper.lua | 2 +- src/shallow/ShallowWrapper.spec.lua | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 92db3107..0a96ce52 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -682,9 +682,9 @@ Similar to `find`, this method will assert that only one child satisfies the giv --- -#### getInstance +#### getHostObject ``` -getInstance() -> Instance or nil +getHostObject() -> Instance or nil ``` Returns the instance object associated with the ShallowWrapper. It can return `nil` if the component wrapped by the ShallowWrapper does not render an instance, but rather another component. Here is an example: @@ -709,11 +709,11 @@ local tree = Roact.mount(element) local wrapper = tree:getShallowWrapper() -print(wrapper:getInstance() == nil) -- prints false +print(wrapper:getHostObject() == nil) -- prints false local coolWrapper = wrapper:findUnique() -print(coolWrapper:getInstance() == nil) -- prints true +print(coolWrapper:getHostObject() == nil) -- prints true ``` --- diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 85df47b7..9bf18d1a 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -146,7 +146,7 @@ function ShallowWrapperPublic:findUnique(constraints) return constrainedChildren[1] end -function ShallowWrapperPublic:getInstance() +function ShallowWrapperPublic:getHostObject() local internalData = self[InternalData] return internalData.instance diff --git a/src/shallow/ShallowWrapper.spec.lua b/src/shallow/ShallowWrapper.spec.lua index e0d06866..7af8722c 100644 --- a/src/shallow/ShallowWrapper.spec.lua +++ b/src/shallow/ShallowWrapper.spec.lua @@ -395,7 +395,7 @@ return function() end) end) - describe("getInstance", function() + describe("getHostObject", function() it("should return the instance when it is a host component", function() local className = "Frame" local function Component(props) @@ -405,7 +405,7 @@ return function() local element = createElement(Component) local wrapper = shallow(element) - local instance = wrapper:getInstance() + local instance = wrapper:getHostObject() expect(instance).to.be.ok() expect(instance.ClassName).to.equal(className) @@ -422,7 +422,7 @@ return function() local element = createElement(Component) local wrapper = shallow(element) - local instance = wrapper:getInstance() + local instance = wrapper:getHostObject() expect(instance).never.to.be.ok() end) From 0699e75bf4b1136b95b2df1d8f7bd97596ac542e Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 15 Aug 2019 18:09:05 -0700 Subject: [PATCH 52/65] Add Rect datatype to serializer --- src/shallow/Snapshot/Serialize/Serializer.lua | 8 ++++++++ src/shallow/Snapshot/Serialize/Serializer.spec.lua | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/shallow/Snapshot/Serialize/Serializer.lua b/src/shallow/Snapshot/Serialize/Serializer.lua index b74a8537..eece1c7a 100644 --- a/src/shallow/Snapshot/Serialize/Serializer.lua +++ b/src/shallow/Snapshot/Serialize/Serializer.lua @@ -75,6 +75,14 @@ function Serializer.tableValue(value) elseif valueType == "EnumItem" then return ("%s"):format(tostring(value)) + elseif valueType == "Rect" then + return ("Rect.new(%s, %s, %s, %s)"):format( + Serializer.number(value.Min.X), + Serializer.number(value.Min.Y), + Serializer.number(value.Max.X), + Serializer.number(value.Max.Y) + ) + elseif valueType == "UDim" then return ("UDim.new(%s, %d)"):format(Serializer.number(value.Scale), value.Offset) diff --git a/src/shallow/Snapshot/Serialize/Serializer.spec.lua b/src/shallow/Snapshot/Serialize/Serializer.spec.lua index 833102ac..b4963477 100644 --- a/src/shallow/Snapshot/Serialize/Serializer.spec.lua +++ b/src/shallow/Snapshot/Serialize/Serializer.spec.lua @@ -133,6 +133,12 @@ return function() expect(result).to.equal("Color3.new(0.1, 0.2, 0.3)") end) + it("should serialize Rect", function() + local result = Serializer.tableValue(Rect.new(0.1, 0.2, 0.3, 0.4)) + + expect(result).to.equal("Rect.new(0.1, 0.2, 0.3, 0.4)") + end) + it("should serialize UDim", function() local result = Serializer.tableValue(UDim.new(1.2, 0)) From d5bcf882be395603837df2cf966771f6bf14a82e Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 16 Aug 2019 16:28:35 -0700 Subject: [PATCH 53/65] Remove hostKey from top component in snapshots --- src/shallow/Snapshot/Serialize/Serializer.lua | 12 ++-- src/shallow/Snapshot/Serialize/Snapshot.lua | 11 ++- .../Snapshot/Serialize/Snapshot.spec.lua | 70 ++++++++++++++++++- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/shallow/Snapshot/Serialize/Serializer.lua b/src/shallow/Snapshot/Serialize/Serializer.lua index eece1c7a..be2eb3f1 100644 --- a/src/shallow/Snapshot/Serialize/Serializer.lua +++ b/src/shallow/Snapshot/Serialize/Serializer.lua @@ -210,14 +210,18 @@ function Serializer.children(children, output) output:popAndWrite("},") end -function Serializer.snapshotDataContent(snapshotData, output) +function Serializer.snapshotDataContent(snapshotData, output, skipHostKey) Serializer.type(snapshotData.type, output) - output:write("hostKey = %q,", snapshotData.hostKey) + + if not skipHostKey then + output:write("hostKey = %q,", snapshotData.hostKey) + end + Serializer.props(snapshotData.props, output) Serializer.children(snapshotData.children, output) end -function Serializer.snapshotData(snapshotData, output) +function Serializer.snapshotData(snapshotData, output, skipHostKey) output:writeAndPush("{") Serializer.snapshotDataContent(snapshotData, output) output:popAndWrite("},") @@ -232,7 +236,7 @@ function Serializer.firstSnapshotData(snapshotData) output:write("") output:writeAndPush("return {") - Serializer.snapshotDataContent(snapshotData, output) + Serializer.snapshotDataContent(snapshotData, output, true) output:popAndWrite("}") output:popAndWrite("end") diff --git a/src/shallow/Snapshot/Serialize/Snapshot.lua b/src/shallow/Snapshot/Serialize/Snapshot.lua index f55c87cc..290c61c3 100644 --- a/src/shallow/Snapshot/Serialize/Snapshot.lua +++ b/src/shallow/Snapshot/Serialize/Snapshot.lua @@ -104,7 +104,7 @@ function Snapshot.children(children) for i=1, #children do local childWrapper = children[i] - serializedChildren[i] = Snapshot.new(childWrapper) + serializedChildren[i] = Snapshot.child(childWrapper) end table.sort(serializedChildren, sortSerializedChildren) @@ -112,7 +112,7 @@ function Snapshot.children(children) return serializedChildren end -function Snapshot.new(wrapper) +function Snapshot.child(wrapper) return { type = Snapshot.type(wrapper.type), hostKey = wrapper.hostKey, @@ -121,4 +121,11 @@ function Snapshot.new(wrapper) } end +function Snapshot.new(wrapper) + local childSnapshot = Snapshot.child(wrapper) + childSnapshot.hostKey = nil + + return childSnapshot +end + return Snapshot diff --git a/src/shallow/Snapshot/Serialize/Snapshot.spec.lua b/src/shallow/Snapshot/Serialize/Snapshot.spec.lua index 551ebd4a..4bca31f0 100644 --- a/src/shallow/Snapshot/Serialize/Snapshot.spec.lua +++ b/src/shallow/Snapshot/Serialize/Snapshot.spec.lua @@ -226,17 +226,83 @@ return function() end) end) - describe("wrapper", function() + describe("child", function() it("should have the host key", function() local hostKey = "SomeKey" local wrapper = shallow(createElement("Frame")) wrapper.hostKey = hostKey - local result = Snapshot.new(wrapper) + local result = Snapshot.child(wrapper) expect(result.hostKey).to.equal(hostKey) end) + it("should contain the element type", function() + local wrapper = shallow(createElement("Frame")) + + local result = Snapshot.child(wrapper) + + expect(result.type).to.be.ok() + expect(result.type.kind).to.equal(ElementKind.Host) + expect(result.type.className).to.equal("Frame") + end) + + it("should contain the props", function() + local props = { + LayoutOrder = 3, + [Change.Size] = function() end, + } + local expectProps = { + LayoutOrder = 3, + [Change.Size] = Markers.AnonymousFunction, + } + + local wrapper = shallow(createElement("Frame", props)) + + local result = Snapshot.child(wrapper) + + expect(result.props).to.be.ok() + assertDeepEqual(result.props, expectProps) + end) + + it("should contain the element children", function() + local wrapper = shallow(createElement("Frame", {}, { + Child = createElement("TextLabel"), + })) + + local result = Snapshot.child(wrapper) + + expect(result.children).to.be.ok() + expect(#result.children).to.equal(1) + local childData = result.children[1] + expect(childData.type.kind).to.equal(ElementKind.Host) + expect(childData.type.className).to.equal("TextLabel") + end) + + it("should sort children by their host key", function() + local wrapper = shallow(createElement("Frame", {}, { + Child = createElement("TextLabel"), + Label = createElement("TextLabel"), + })) + + local result = Snapshot.child(wrapper) + + expect(result.children).to.be.ok() + expect(#result.children).to.equal(2) + expect(result.children[1].hostKey).to.equal("Child") + expect(result.children[2].hostKey).to.equal("Label") + end) + end) + + describe("new", function() + it("should clear the host key", function() + local wrapper = shallow(createElement("Frame")) + + local result = Snapshot.new(wrapper) + + expect(result.hostKey).never.to.be.ok() + end) + it("should contain the element type", function() local wrapper = shallow(createElement("Frame")) From 90010ec8f2e504d7c5353140aab8536953e45e1b Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 16 Aug 2019 16:30:50 -0700 Subject: [PATCH 54/65] Add depth and find/findUnique examples --- docs/advanced/shallow-rendering.md | 320 +++++++++++++++++++++++++++-- docs/api-reference.md | 1 - 2 files changed, 303 insertions(+), 18 deletions(-) diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index 14eb1c32..c0988bbe 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -12,15 +12,10 @@ local tree = Roact.mount(ComponentToTest) local shallowWrapper = tree:getShallowWrapper() ``` -### TODO: Testing Shallows Using Find -We should have a section on how and when to use find/findUnique - -## Snapshots +## Snapshot Testing A snapshot is a serializable representation of a shallow rendered Roact tree that can be serialized as a complete Lua `ModuleScript`. The snapshot can either be compared with a past snapshot written to `ReplicatedStorage` or returned as a string for debugging purposes. -### Snapshot Testing - The goal of a snapshot test is to verify that the shallow render matches the version saved previously. If a snapshot does not match, then the component may have changed unexpectedly. Snapshot testing can be done through the `matchSnapshot` method on the `ShallowWrapper`. The single argument of `matchSnapshot` is the name of the `ModuleScript` in `ReplicatedStorage` to look for. ```lua @@ -42,7 +37,7 @@ Here is a breakdown of how matching is performed: The scripts generated by `matchSnapshot` are not guaranteed to pass [luacheck](https://github.com/mpeterv/luacheck/) due to the potential for unused variables, so be careful not to run `luacheck` on them. --- -#### Workflow Example +### Workflow Example For a concrete example, suppose the following component `ComponentToTest`. @@ -79,7 +74,6 @@ return function(dependencies) kind = ElementKind.Host, className = "TextLabel", }, - hostKey = "RoactTree", props = { Text = "foo", }, @@ -113,7 +107,6 @@ return function(dependencies) kind = ElementKind.Host, className = "TextLabel", }, - hostKey = "RoactTree", props = { Text = "bar", }, @@ -130,21 +123,21 @@ Most snapshots will be more complex than this example and act as a powerful line --- -#### Where They Are Good +### Where They Are Good -##### Regression +#### Regression Snapshot tests really shine when comes the time to test for regression. -##### Carefully Reviewed +#### Carefully Reviewed Changes made to a snapshot file needs to be reviewed carefully as if it was hand written code. A reviewer needs to be able to catch any unexpected changes to a component. Any source control software should provide some way to see a diff of the changes that are going to be submitted. If a snapshot diff shows a difference in the color property for a change that is supposed to update sizing, the reviewer should verify that the change is intended. --- -#### Where They Are Bad +### Where They Are Bad -##### Large Snapshots +#### Large Snapshots If a snapshot is created from a top level component with a ShallowWrapper that renders many levels deep, it can produce a large snapshot file with potentially hundreds of lines. The larger the snapshot, the more likely it is to fail due to a reason unrelated to the component being tested. @@ -154,19 +147,312 @@ To avoid this situation, it is important that each snapshot is kept as simple an --- -#### Managing Snapshots +### Managing Snapshots When the tests are executed in Run mode (after Run is pressed), snapshots are serialized and saved as `StringValue` objects inside of `ReplicatedStorage.RoactSnapshots`. Pressing Stop to go back to Edit mode will delete any newly created snapshots values. Preserving the serialized snapshots and saving them as module scripts is necessary to ensure that there are snapshots to match with during future test runs. The method of preserving them varies based on the development environment being used. -##### Roblox Studio +#### Roblox Studio If using Roblox Studio for development, install the `RoactSnapshots` plugin, which will preserve the `StringValue` objects and save them as `ModuleScript` objects upon returning to Edit mode. --- -##### File System +#### File System If using a tool like `rojo` to sync files to Roblox Studio, a tool like `run-in-roblox` can help write the module scripts back to the file system. [`run-in-roblox`](https://github.com/LPGhatguy/run-in-roblox/) is a [`Rust`](https://www.rust-lang.org/) command line tool that runs Roblox Studio and sends content from the output to the shell window. Using this tool, a script can be written to open a place file and run it with a specific test runner that can print out the new snapshots in a special format. Then, the output can be parsed to find the new snapshots and write them to files. Here are examples of this kind of script written in Lua ([link](../scripts/sync-snapshots-with-lua.md)) and in python ([link](../scripts/sync-snapshots-with-python.md)) (compatible with version 2 and 3). These example scripts assume that the `rojo` and `run-in-roblox` commands are available. They build a place from a `rojo` configuration file, run a specific script inside studio, print the serialized snapshots, parse them from the output, and write them to the file system. + +--- + +### Wrapped Components + +Sometimes during testing, components need to be wrapped into other components that provides some of their props. When comes the time to make a snapshot of that components, it's preferable to snapshot it without the wrapping component. The main reason for this is to avoid snapshot test failure when there is a change in the wrapping component that does not affect the wrapped component. + +In the case where the component is wrapped in a host component (a component that render a Roblox instance), use ShallowWrapper's [`find`](/api-reference/#find) or [`findUnique`](/api-reference/#findunique) methods to get to your child component. Otherwise, increase the depth option to get deep enough so that the parent component is consumed. + +!!! Note + When making snapshot tests, depth is generally used more often than `find` or `findUnique`. Components may have multiple layer of elements nested: that will require adjusting the depth to render at least until the last element. The ShallowWrapper's methods that access its children are more used when working on generic components. These generic components may be a bit harder or tricky to test because they depend on other concrete components that get wrappred into them. + +--- + +#### Example With *find* Method + +Let's suppose `InFrame` is a component that accept a render prop. The render function is used to render an element inside a black background frame. The component will pass props that need to be set on the child element. + +```lua +local function InFrame(props) + return Roact.createElement("Frame", { + BackgroundColor3 = Color3.new(0, 0, 0), + }, { + Content = props.render({ + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + }), + }) +end +``` + +Another component called `CoolComponent` is using this generic component to render a TextLabel inside a frame. + +```lua +local function CoolComponent() + return Roact.createElement(InFrame, { + render = function(props) + props.Text = "Hello" + return Roact.createElement("TextLabel", props) + end, + }) +end +``` + +When writting a snapshot test for `CoolComponent`, it is better to avoid having details about the `InFrame` component. The important things are to be aware of what could change in the relation between `InFrame` and `CoolComponent`. Any changes made to `InFrame` that does not impact `CoolComponent` should not break the snapshot. + +The first version of the test may look like the following snippet: + +```lua +local element = Roact.createElement(CoolComponent) + +local tree = Roact.mount(element) + +local wrapper = tree:getShallowWrapper() + +wrapper:matchSnapshot("CoolComponent") +``` + +That will produce the following snapshot: + +```lua +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Function, + }, + props = { + render = Markers.AnonymousFunction, + }, + children = { + { + type = { + kind = ElementKind.Host, + className = "Frame", + }, + hostKey = "RoactTree", + props = { + BackgroundColor3 = Color3.new(0, 0, 0), + }, + children = {}, + }, + }, + } +end +``` + +As you can see, this snapshot contains the `render` prop of `InFrame`. The information about the TextLabel is also absent, because the ShallowWrapper has a depth of one by default and the TextLabel is getting wrapped into a Frame. In order to get the TextLabel to show up in the snapshot, the depth is going to be bumped to 2. The updated test will look like: + +```lua +local element = Roact.createElement(CoolComponent) + +local tree = Roact.mount(element) + +local wrapper = tree:getShallowWrapper({ + depth = 2, +}) + +wrapper:matchSnapshot("CoolComponent") +``` + +And the updated snapshot is going to contain data about the TextLabel. + +```lua +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "Frame", + }, + props = { + BackgroundColor3 = Color3.new(0, 0, 0), + }, + children = { + { + type = { + kind = ElementKind.Host, + className = "TextLabel", + }, + hostKey = "Content", + props = { + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + Text = "Hello", + }, + children = {}, + }, + }, + } +end +``` + +Now, to remove the details about the `InFrame` component, the snapshot is not going to be generated from the top level ShallowWrapper, but instead from the child inside of it. + +```lua +local element = Roact.createElement(CoolComponent) + +local tree = Roact.mount(element) + +local wrapper = tree:getShallowWrapper({ + depth = 2, +}) + +local coolWrapper = wrapper:findUnique() + +coolWrapper:matchSnapshot("CoolComponent") +``` + +Since the top level Frame contains only one child, we can use `findUnique` instead of `find`. This will produce the following snapshot: + +```lua +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "TextLabel", + }, + props = { + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(1, 0, 1, 0), + Text = "Hello", + }, + children = {}, + } +end +``` + +The good thing now about this snapshot is that it will only fail when the contract between `InFrame` and `CoolComponent` changes. + +--- + +#### Example With Depth + +Let's say `CoolComponent` is using another component that wraps it with a render prop. The consumer is going to pass a table containing data about the style. For the purpose of this example, the props are minimal but it still shows the idea. + +```lua +local function CoolComponent(props) + return Roact.createElement(StyleConsumer, { + render = function(style) + return Roact.createElement("Frame", { + BackgroundColor3 = style.MainColor, + }) + end, + }) +end +``` + +Usually, this complexity is hidden behind a simple function like the following snippet. + +```lua +local function CoolComponent(props) + return withStyle(function(style) + return Roact.createElement("Frame", { + BackgroundColor3 = style.MainColor, + }, { + Label = Roact.createElement("TextLabel"), + }) + end) +end +``` + +We can write a test that is going to snapshot the `CoolComponent`. + +```lua +local element = Roact.createElement(CoolComponent) + +local tree = Roact.mount(element) +local wrapper = tree:getShallowWrapper() + +wrapper:matchSnapshot("CoolSnapshot") +``` + +And the generated snapshot looks like this: + +```lua +{ + type = { + kind = ElementKind.Function, + }, + props = { + render = Markers.AnonymousFunction, -- the render prop from the StyleConsumer + }, + children = { + { + type = { + kind = ElementKind.Host, + className = "Frame", + }, + hostKey = "RoactTree", + props = { + BackgroundColor3 = Color3.new(1, 1, 1), -- style.MainColor + }, + children = {}, + }, + }, +} +``` + +When writing the test to snapshot `CoolComponent`, you will want to use to avoid having details of the `StyleConsumer`. The reason is that if `StyleConsumer` changes, the test may fail without really changing the important part of our snapshot, which is the information about `CoolComponent`. + +Also, notice that there is no information about the `TextLabel`, because the method `getShallowWrapper` only returns a shallow version of the mounted tree. If we want to have access to the next depth of the tree, we need to pass the option `depth = 2`. + +```lua +local element = Roact.createElement(CoolComponent) + +local tree = Roact.mount(element) +local wrapper = tree:getShallowWrapper({ + depth = 2, +}) + +wrapper:matchSnapshot("CoolSnapshot") +``` + +We get the following new generated snapshot. + +```lua +{ + type = { + kind = ElementKind.Host, + className = "Frame", + }, + props = { + BackgroundColor3 = Color3.new(1, 1, 1), + }, + children = { + { + type = { + kind = ElementKind.Host, + className = "TextLabel", + }, + hostKey = "Label", + props = {}, + children = {}, + }, + }, +} +``` + +As you can see, the `StyleConsumer` disappeared. The wrapper returned by `getShallowWrapper` does not directly wrap the element tree. Instead, it wraps elements that are rendered as Roblox instances as deep as it can. Since, the test now allow the wrapper to go deeper, it will get rid of the `StyleConsumer` because it does not render to a Roblox instance. diff --git a/docs/api-reference.md b/docs/api-reference.md index 0a96ce52..11dda546 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -681,7 +681,6 @@ Similar to `find`, this method will assert that only one child satisfies the giv --- - #### getHostObject ``` getHostObject() -> Instance or nil From 5b99367d8a7a04871f4b7d9db682d995f06db8e8 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 16 Aug 2019 17:09:46 -0700 Subject: [PATCH 55/65] Throw error when generating snapshots --- docs/api-reference.md | 6 ++- src/shallow/Snapshot/SnapshotMatcher.lua | 16 ++++--- src/shallow/Snapshot/SnapshotMatcher.spec.lua | 47 +++++++++++-------- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 11dda546..cecd8ed6 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -721,10 +721,12 @@ print(coolWrapper:getHostObject() == nil) -- prints true ``` matchSnapshot(identifier) ``` -If no previous snapshot with the given identifier exists, it will create a new StringValue instance that will contain Lua code representing the current ShallowWrapper. When an existing snapshot is found (a ModuleScript named as the provided identifier), it will require the ModuleScript and load the data from it. Then, if the loaded data is different from the current ShallowWrapper, an error will be thrown. +When an existing snapshot with the given identifier is found (a ModuleScript named as the provided identifier), it will require the ModuleScript and load the data from it. Then, if the loaded data is different from the current ShallowWrapper, an error will be thrown. If no previous snapshot exists, it will throw error. + +When an error is thrown, a StringValue instance is created with the given identifier followed by *.NEW*. Its Value property will contain Lua code representing the current ShallowWrapper. !!! note - As mentionned, `matchSnapshot` will create a StringValue, named like the given identifier, in which the generated lua code will be assigned to the Value property. When these values are generated in Studio during run mode, it's important to copy back the values and convert them into ModuleScripts. + As mentionned, `matchSnapshot` may create a new StringValue, named like the given identifier, in which the generated lua code will be assigned to the Value property. When these values are generated in Studio during run mode, it's important to copy back the values and convert them into ModuleScripts. --- diff --git a/src/shallow/Snapshot/SnapshotMatcher.lua b/src/shallow/Snapshot/SnapshotMatcher.lua index 1560bbd7..d88d73bf 100644 --- a/src/shallow/Snapshot/SnapshotMatcher.lua +++ b/src/shallow/Snapshot/SnapshotMatcher.lua @@ -14,6 +14,13 @@ local SnapshotMetatable = { __index = SnapshotMatcher, } +local function throwSnapshotError(matcher, message) + local newSnapshot = SnapshotMatcher.new(matcher._identifier .. ".NEW", matcher._snapshot) + newSnapshot:serialize() + + error(message, 3) +end + function SnapshotMatcher.new(identifier, snapshot) local snapshotMatcher = { _identifier = identifier, @@ -28,9 +35,7 @@ end function SnapshotMatcher:match() if self._existingSnapshot == nil then - self:serialize() - self._existingSnapshot = self._snapshot - return + throwSnapshotError(self, ("Snapshot %q not found"):format(self._identifier)) end local areEqual, innerMessageTemplate = deepEqual(self._snapshot, self._existingSnapshot) @@ -39,16 +44,13 @@ function SnapshotMatcher:match() return end - local newSnapshot = SnapshotMatcher.new(self._identifier .. ".NEW", self._snapshot) - newSnapshot:serialize() - local innerMessage = innerMessageTemplate :gsub("{1}", "new") :gsub("{2}", "existing") local message = ("Snapshots do not match.\n%s"):format(innerMessage) - error(message, 2) + throwSnapshotError(self, message) end function SnapshotMatcher:serialize() diff --git a/src/shallow/Snapshot/SnapshotMatcher.spec.lua b/src/shallow/Snapshot/SnapshotMatcher.spec.lua index be44e88f..d5200d22 100644 --- a/src/shallow/Snapshot/SnapshotMatcher.spec.lua +++ b/src/shallow/Snapshot/SnapshotMatcher.spec.lua @@ -13,6 +13,17 @@ return function() return snapshotFolder end + local function getSnapshotMock() + return { + type = { + kind = ElementKind.Function, + }, + hostKey = "HostKey", + props = {}, + children = {}, + } + end + local originalLoadExistingData = SnapshotMatcher._loadExistingData local loadExistingDataSpy = nil @@ -26,33 +37,38 @@ return function() return snapshotMap[identifier] end) SnapshotMatcher._loadExistingData = loadExistingDataSpy.value + SnapshotMatcher.getSnapshotFolder = mockGetSnapshotFolder end local function cleanTest() loadExistingDataSpy = nil SnapshotMatcher._loadExistingData = originalLoadExistingData + SnapshotMatcher.getSnapshotFolder = originalGetSnapshotFolder + snapshotFolder:ClearAllChildren() end - it("should serialize the snapshot if no data is found", function() + it("should throw if no snapshot is found", function() beforeTest() - local snapshot = {} - local serializeSpy = createSpy() + local snapshot = getSnapshotMock() local matcher = SnapshotMatcher.new("foo", snapshot) - matcher.serialize = serializeSpy.value - matcher:match() + local function shouldThrow() + matcher:match() + end - cleanTest() + expect(shouldThrow).to.throw() - serializeSpy:assertCalledWith(matcher) + expect(snapshotFolder:FindFirstChild("foo.NEW")).to.be.ok() + + cleanTest() end) it("should not serialize if the snapshot already exist", function() beforeTest() - local snapshot = {} + local snapshot = getSnapshotMock() local identifier = "foo" snapshotMap[identifier] = snapshot @@ -71,7 +87,7 @@ return function() it("should throw an error if the previous snapshot does not match", function() beforeTest() - local snapshot = {} + local snapshot = getSnapshotMock() local identifier = "foo" snapshotMap[identifier] = { Key = "Value" @@ -86,9 +102,9 @@ return function() matcher:match() end - cleanTest() - expect(shouldThrow).to.throw() + + cleanTest() end) end) @@ -98,14 +114,7 @@ return function() local identifier = "foo" - local matcher = SnapshotMatcher.new(identifier, { - type = { - kind = ElementKind.Function, - }, - hostKey = "HostKey", - props = {}, - children = {}, - }) + local matcher = SnapshotMatcher.new(identifier, getSnapshotMock()) matcher:serialize() local stringValue = snapshotFolder:FindFirstChild(identifier) From 27db6d98039de3a67a7313344fc55cb8c4a99537 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Fri, 16 Aug 2019 17:34:12 -0700 Subject: [PATCH 56/65] Update shallow rendering docs --- docs/advanced/shallow-rendering.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index c0988bbe..a577195a 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -61,7 +61,7 @@ it("should match the snapshot", function() end) ``` -After the first run, the test will have created a new script under `RoactSnapshots` in `ReplicatedStorage` called `ComponentToTest` that contains the following Lua code: +After the first run, the test will fail and create a new script under `RoactSnapshots` in `ReplicatedStorage` called `ComponentToTest.NEW` that contains the following Lua code: ```lua return function(dependencies) @@ -82,7 +82,10 @@ return function(dependencies) end ``` -Since these tests require the previous snapshots to compare with the current generated one, snapshots should be saved (if using Studio) or committed to version control (if using file system development). +After reviewing the snapshot, it can be renamed to `ComponentToTest`. If the test is run again it will now pass since the serialized snapshot match the generated one. + +!!! Note + Since snapshot tests require the previous snapshots to compare with the current generated one, snapshots should be saved (if using Studio) or committed to version control (if using file system development). Suppose now `ComponentToTest` is updated as follows: @@ -94,7 +97,7 @@ local function ComponentToTest(props) end ``` -When the test is run again, it will fail, noting that the snapshots did not match. There will be a new script under `RoactSnapshots` called `ComponentToTest.NEW` that shows the new version of the snapshot. +When the test is run again, it will fail because that the snapshots did not match. There will be a new script under `RoactSnapshots` called `ComponentToTest.NEW` that shows the new version of the snapshot. ```lua return function(dependencies) From 1b1553d8893ef4c447d72e7334e4ea3329891596 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Mon, 19 Aug 2019 09:24:58 -0700 Subject: [PATCH 57/65] Add snapshots files --- RoactSnapshots/component-with-event-props.lua | 19 ++++++++++ .../function-component-children.lua | 35 +++++++++++++++++++ .../host-frame-with-multiple-props.lua | 23 ++++++++++++ .../stateful-component-children.lua | 28 +++++++++++++++ bin/spec.lua | 1 + place.project.json | 4 +++ 6 files changed, 110 insertions(+) create mode 100644 RoactSnapshots/component-with-event-props.lua create mode 100644 RoactSnapshots/function-component-children.lua create mode 100644 RoactSnapshots/host-frame-with-multiple-props.lua create mode 100644 RoactSnapshots/stateful-component-children.lua diff --git a/RoactSnapshots/component-with-event-props.lua b/RoactSnapshots/component-with-event-props.lua new file mode 100644 index 00000000..f75ea95e --- /dev/null +++ b/RoactSnapshots/component-with-event-props.lua @@ -0,0 +1,19 @@ +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "TextButton", + }, + props = { + [Roact.Event.Activated] = Markers.AnonymousFunction, + [Roact.Event.MouseButton1Click] = Markers.AnonymousFunction, + [Roact.Change.AbsoluteSize] = Markers.AnonymousFunction, + [Roact.Change.Visible] = Markers.AnonymousFunction, + }, + children = {}, + } +end \ No newline at end of file diff --git a/RoactSnapshots/function-component-children.lua b/RoactSnapshots/function-component-children.lua new file mode 100644 index 00000000..3e114236 --- /dev/null +++ b/RoactSnapshots/function-component-children.lua @@ -0,0 +1,35 @@ +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "Frame", + }, + props = {}, + children = { + { + type = { + kind = ElementKind.Function, + }, + hostKey = "LabelA", + props = { + Text = "I am label A", + }, + children = {}, + }, + { + type = { + kind = ElementKind.Function, + }, + hostKey = "LabelB", + props = { + Text = "I am label B", + }, + children = {}, + }, + }, + } +end \ No newline at end of file diff --git a/RoactSnapshots/host-frame-with-multiple-props.lua b/RoactSnapshots/host-frame-with-multiple-props.lua new file mode 100644 index 00000000..1a27dde3 --- /dev/null +++ b/RoactSnapshots/host-frame-with-multiple-props.lua @@ -0,0 +1,23 @@ +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "Frame", + }, + props = { + AnchorPoint = Vector2.new(0, 0.5), + BackgroundColor3 = Color3.new(0.1, 0.2, 0.3), + BackgroundTransparency = 0.205, + ClipsDescendants = false, + Size = UDim2.new(0.5, 0, 0.4, 1), + SizeConstraint = Enum.SizeConstraint.RelativeXY, + Visible = true, + ZIndex = 5, + }, + children = {}, + } +end \ No newline at end of file diff --git a/RoactSnapshots/stateful-component-children.lua b/RoactSnapshots/stateful-component-children.lua new file mode 100644 index 00000000..588f142c --- /dev/null +++ b/RoactSnapshots/stateful-component-children.lua @@ -0,0 +1,28 @@ +return function(dependencies) + local Roact = dependencies.Roact + local ElementKind = dependencies.ElementKind + local Markers = dependencies.Markers + + return { + type = { + kind = ElementKind.Host, + className = "Frame", + }, + props = {}, + children = { + { + type = { + kind = ElementKind.Stateful, + componentName = "CoolComponent", + }, + hostKey = "Child", + props = { + label = { + Text = "foo", + }, + }, + children = {}, + }, + }, + } +end \ No newline at end of file diff --git a/bin/spec.lua b/bin/spec.lua index b6d0f792..d900be8d 100644 --- a/bin/spec.lua +++ b/bin/spec.lua @@ -6,6 +6,7 @@ local LOAD_MODULES = { {"src", "Roact"}, {"modules/testez/lib", "TestEZ"}, + {"RoactSnapshots", "RoactSnapshots"}, } -- This makes sure we can load Lemur and other libraries that depend on init.lua diff --git a/place.project.json b/place.project.json index eaec7943..ad2e5a68 100644 --- a/place.project.json +++ b/place.project.json @@ -8,6 +8,10 @@ "Roact": { "$path": "src" + }, + + "RoactSnapshots": { + "$path": "RoactSnapshots" }, "TestEZ": { From 13edb92d17f60bea51d2c2296239c3e5656044d7 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 20 Aug 2019 10:26:16 -0700 Subject: [PATCH 58/65] Remove generated trailing whitespaces in snapshots --- RoactSnapshots/function-component-children.lua | 2 +- RoactSnapshots/host-frame-with-multiple-props.lua | 2 +- RoactSnapshots/stateful-component-children.lua | 2 +- src/shallow/Snapshot/Serialize/IndentedOutput.lua | 14 ++++++++++---- .../Snapshot/Serialize/IndentedOutput.spec.lua | 11 +++++++++++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/RoactSnapshots/function-component-children.lua b/RoactSnapshots/function-component-children.lua index 3e114236..65b1146d 100644 --- a/RoactSnapshots/function-component-children.lua +++ b/RoactSnapshots/function-component-children.lua @@ -2,7 +2,7 @@ return function(dependencies) local Roact = dependencies.Roact local ElementKind = dependencies.ElementKind local Markers = dependencies.Markers - + return { type = { kind = ElementKind.Host, diff --git a/RoactSnapshots/host-frame-with-multiple-props.lua b/RoactSnapshots/host-frame-with-multiple-props.lua index 1a27dde3..7e1f6ff0 100644 --- a/RoactSnapshots/host-frame-with-multiple-props.lua +++ b/RoactSnapshots/host-frame-with-multiple-props.lua @@ -2,7 +2,7 @@ return function(dependencies) local Roact = dependencies.Roact local ElementKind = dependencies.ElementKind local Markers = dependencies.Markers - + return { type = { kind = ElementKind.Host, diff --git a/RoactSnapshots/stateful-component-children.lua b/RoactSnapshots/stateful-component-children.lua index 588f142c..c2e037ac 100644 --- a/RoactSnapshots/stateful-component-children.lua +++ b/RoactSnapshots/stateful-component-children.lua @@ -2,7 +2,7 @@ return function(dependencies) local Roact = dependencies.Roact local ElementKind = dependencies.ElementKind local Markers = dependencies.Markers - + return { type = { kind = ElementKind.Host, diff --git a/src/shallow/Snapshot/Serialize/IndentedOutput.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.lua index c4ccd3ae..0d33c896 100644 --- a/src/shallow/Snapshot/Serialize/IndentedOutput.lua +++ b/src/shallow/Snapshot/Serialize/IndentedOutput.lua @@ -3,13 +3,17 @@ local IndentedOutputMetatable = { __index = IndentedOutput, } -function IndentedOutput.new(indentation) +function IndentedOutput.new(indentation, removeTrailingWhitespaces) indentation = indentation or 2 + if removeTrailingWhitespaces == nil then + removeTrailingWhitespaces = true + end local output = { _level = 0, _indentation = (" "):rep(indentation), _lines = {}, + _removeTrailingWhitespaces = removeTrailingWhitespaces, } setmetatable(output, IndentedOutputMetatable) @@ -22,9 +26,11 @@ function IndentedOutput:write(line, ...) line = line:format(...) end - local indentedLine = ("%s%s"):format(self._indentation:rep(self._level), line) - - table.insert(self._lines, indentedLine) + if self._removeTrailingWhitespaces and line == "" then + table.insert(self._lines, line) + else + table.insert(self._lines, ("%s%s"):format(self._indentation:rep(self._level), line)) + end end function IndentedOutput:push() diff --git a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua index 7f9ffbe1..9eaa9fb7 100644 --- a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua +++ b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua @@ -31,6 +31,17 @@ return function() expect(output:join()).to.equal("foo\n bar") end) + + it("should remove trailing whitespaces", function() + local output = IndentedOutput.new() + + output:push() + output:write("foo") + output:write("") + output:write("bar") + + expect(output:join()).to.equal(" foo\n\n bar") + end) end) describe("pop", function() From ead931bbbaea8b32b8831293ade73099a6443e7f Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 20 Aug 2019 10:29:25 -0700 Subject: [PATCH 59/65] Move trailing whitespace tests --- .../Snapshot/Serialize/IndentedOutput.spec.lua | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua index 9eaa9fb7..1c14ee63 100644 --- a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua +++ b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua @@ -21,26 +21,28 @@ return function() end) end) - describe("push", function() - it("should indent next written lines", function() + describe("write", function() + it("should remove trailing whitespaces", function() local output = IndentedOutput.new() - output:write("foo") output:push() + output:write("foo") + output:write("") output:write("bar") - expect(output:join()).to.equal("foo\n bar") + expect(output:join()).to.equal(" foo\n\n bar") end) + end) - it("should remove trailing whitespaces", function() + describe("push", function() + it("should indent next written lines", function() local output = IndentedOutput.new() - output:push() output:write("foo") - output:write("") + output:push() output:write("bar") - expect(output:join()).to.equal(" foo\n\n bar") + expect(output:join()).to.equal("foo\n bar") end) end) From 6dc2133126c9ce823308b65b7ac5abba1102532f Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 20 Aug 2019 17:33:15 -0700 Subject: [PATCH 60/65] Remove ElementKind from ShallowWrapper In order to prevent leaking the ElementKind symbols, they have been removed from the ShallowWrapper API --- docs/api-reference.md | 18 +++---- src/VirtualTree.spec.lua | 2 +- src/shallow/ShallowWrapper.lua | 24 +++------ src/shallow/ShallowWrapper.spec.lua | 54 +++++-------------- src/shallow/Snapshot/Serialize/Snapshot.lua | 16 +++--- .../Snapshot/Serialize/Snapshot.spec.lua | 10 ++-- 6 files changed, 40 insertions(+), 84 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index cecd8ed6..12b4c9fc 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -616,20 +616,14 @@ As with `setState`, you can set use the constant `Roact.None` to remove a field ### Fields -#### type -``` -type: { - kind: ElementKind -} -``` - -The type dictionary always has the `kind` field that tell the component type. Additionally, depending of the kind of component, other information can be included. +#### component +The component field can be a string, a function or a table depending of the kind of component. For example, if the ShallowWrapper comes from a host component, the component field will contain the class name of the host object created. The following table summarize the different cases. -| kind | fields | description | +| field type | kind | description | | --- | --- | --- | -| Host | className: string | the ClassName of the instance | -| Function | functionComponent: function | the function that renders the element | -| Stateful | component: table | the class-like table used to render the element | +| string | Host | the ClassName of the instance | +| function | Function | the function that renders the element | +| table | Stateful | the class-like table used to render the element | --- diff --git a/src/VirtualTree.spec.lua b/src/VirtualTree.spec.lua index 7037a929..8e4329a5 100644 --- a/src/VirtualTree.spec.lua +++ b/src/VirtualTree.spec.lua @@ -43,7 +43,7 @@ return function() local wrapper = tree:getShallowWrapper() expect(wrapper).to.be.ok() - expect(wrapper.type.className).to.equal("StringValue") + expect(wrapper.component).to.equal("StringValue") end) end) end \ No newline at end of file diff --git a/src/shallow/ShallowWrapper.lua b/src/shallow/ShallowWrapper.lua index 9bf18d1a..dadac21f 100644 --- a/src/shallow/ShallowWrapper.lua +++ b/src/shallow/ShallowWrapper.lua @@ -16,25 +16,15 @@ local ShallowWrapperMetatable = { __index = ShallowWrapperPublic, } -local function getTypeFromVirtualNode(virtualNode) +local function getComponentFromVirtualNode(virtualNode) local element = virtualNode.currentElement local kind = ElementKind.of(element) - if kind == ElementKind.Host then - return { - kind = ElementKind.Host, - className = element.component, - } - elseif kind == ElementKind.Function then - return { - kind = ElementKind.Function, - functionComponent = element.component, - } - elseif kind == ElementKind.Stateful then - return { - kind = ElementKind.Stateful, - component = element.component, - } + if kind == ElementKind.Host + or kind == ElementKind.Function + or kind == ElementKind.Stateful + then + return element.component else error(("shallow wrapper does not support element of kind %q"):format(tostring(kind))) end @@ -97,7 +87,7 @@ function ShallowWrapper.new(virtualNode, maxDepth) local wrapper = { [InternalData] = internalData, - type = getTypeFromVirtualNode(virtualNode), + component = getComponentFromVirtualNode(virtualNode), props = filterProps(virtualNode.currentElement.props), hostKey = virtualNode.hostKey, children = {}, diff --git a/src/shallow/ShallowWrapper.spec.lua b/src/shallow/ShallowWrapper.spec.lua index 7af8722c..96477392 100644 --- a/src/shallow/ShallowWrapper.spec.lua +++ b/src/shallow/ShallowWrapper.spec.lua @@ -4,7 +4,6 @@ return function() local assertDeepEqual = require(RoactRoot.assertDeepEqual) local Children = require(RoactRoot.PropMarkers.Children) - local ElementKind = require(RoactRoot.ElementKind) local createElement = require(RoactRoot.createElement) local createFragment = require(RoactRoot.createFragment) local createReconciler = require(RoactRoot.createReconciler) @@ -31,20 +30,12 @@ return function() return createElement(className, props) end - it("should have it's type.kind to Host", function() + it("should have its component set to the given instance class", function() local element = createElement(Component) local result = shallow(element) - expect(result.type.kind).to.equal(ElementKind.Host) - end) - - it("should have its type.className to given instance class", function() - local element = createElement(Component) - - local result = shallow(element) - - expect(result.type.className).to.equal(className) + expect(result.component).to.equal(className) end) it("children count should be zero", function() @@ -65,20 +56,12 @@ return function() return createElement(FunctionComponent, props) end - it("should have its type.kind to Function", function() - local element = createElement(Component) - - local result = shallow(element) - - expect(result.type.kind).to.equal(ElementKind.Function) - end) - - it("should have its type.functionComponent to Function", function() + it("should have its component set to the functional component", function() local element = createElement(Component) local result = shallow(element) - expect(result.type.functionComponent).to.equal(FunctionComponent) + expect(result.component).to.equal(FunctionComponent) end) end) @@ -93,20 +76,12 @@ return function() return createElement(StatefulComponent, props) end - it("should have its type.kind to Stateful", function() - local element = createElement(Component) - - local result = shallow(element) - - expect(result.type.kind).to.equal(ElementKind.Stateful) - end) - - it("should have its type.component to given component class", function() + it("should have its component set to the given component class", function() local element = createElement(Component) local result = shallow(element) - expect(result.type.component).to.equal(StatefulComponent) + expect(result.component).to.equal(StatefulComponent) end) end) @@ -138,8 +113,7 @@ return function() depth = 3, }) - expect(result.type.kind).to.equal(ElementKind.Host) - expect(result.type.className).to.equal(unwrappedClassName) + expect(result.component).to.equal(unwrappedClassName) end) it("should stop unwrapping function components when depth has exceeded", function() @@ -149,8 +123,7 @@ return function() depth = 2, }) - expect(result.type.kind).to.equal(ElementKind.Function) - expect(result.type.functionComponent).to.equal(A) + expect(result.component).to.equal(A) end) it("should not unwrap the element when depth is zero", function() @@ -160,8 +133,7 @@ return function() depth = 0, }) - expect(result.type.kind).to.equal(ElementKind.Function) - expect(result.type.functionComponent).to.equal(Component) + expect(result.component).to.equal(Component) end) it("should not unwrap children when depth is one", function() @@ -311,7 +283,6 @@ return function() local result = shallow(element) - expect(result.type.kind).to.equal(ElementKind.Host) expect(result.props).to.be.ok() assertDeepEqual(props, result.props) @@ -338,7 +309,7 @@ return function() local result = shallow(element) - expect(result.type.kind).to.equal(ElementKind.Function) + expect(result.component).to.equal(ChildComponent) expect(result.props).to.be.ok() assertDeepEqual(propsCopy, result.props) @@ -526,8 +497,7 @@ return function() local child = result:findUnique() - expect(child.type.kind).to.equal(ElementKind.Host) - expect(child.type.className).to.equal("TextLabel") + expect(child.component).to.equal("TextLabel") end) it("should return the only child that satifies the constraint", function() @@ -542,7 +512,7 @@ return function() className = "TextLabel", }) - expect(child.type.className).to.equal("TextLabel") + expect(child.component).to.equal("TextLabel") end) it("should throw if there is not any child element", function() diff --git a/src/shallow/Snapshot/Serialize/Snapshot.lua b/src/shallow/Snapshot/Serialize/Snapshot.lua index 290c61c3..6ac9a7a6 100644 --- a/src/shallow/Snapshot/Serialize/Snapshot.lua +++ b/src/shallow/Snapshot/Serialize/Snapshot.lua @@ -11,15 +11,17 @@ end local Snapshot = {} -function Snapshot.type(wrapperType) +function Snapshot.type(wrapperComponent) + local kind = ElementKind.fromComponent(wrapperComponent) + local typeData = { - kind = wrapperType.kind, + kind = kind, } - if wrapperType.kind == ElementKind.Host then - typeData.className = wrapperType.className - elseif wrapperType.kind == ElementKind.Stateful then - typeData.componentName = tostring(wrapperType.component) + if kind == ElementKind.Host then + typeData.className = wrapperComponent + elseif kind == ElementKind.Stateful then + typeData.componentName = tostring(wrapperComponent) end return typeData @@ -114,7 +116,7 @@ end function Snapshot.child(wrapper) return { - type = Snapshot.type(wrapper.type), + type = Snapshot.type(wrapper.component), hostKey = wrapper.hostKey, props = Snapshot.props(wrapper.props), children = Snapshot.children(wrapper.children), diff --git a/src/shallow/Snapshot/Serialize/Snapshot.spec.lua b/src/shallow/Snapshot/Serialize/Snapshot.spec.lua index 4bca31f0..dbb42368 100644 --- a/src/shallow/Snapshot/Serialize/Snapshot.spec.lua +++ b/src/shallow/Snapshot/Serialize/Snapshot.spec.lua @@ -35,7 +35,7 @@ return function() it("should contain the host kind", function() local wrapper = shallow(createElement("Frame")) - local result = Snapshot.type(wrapper.type) + local result = Snapshot.type(wrapper.component) expect(result.kind).to.equal(ElementKind.Host) end) @@ -44,7 +44,7 @@ return function() local className = "Frame" local wrapper = shallow(createElement(className)) - local result = Snapshot.type(wrapper.type) + local result = Snapshot.type(wrapper.component) expect(result.className).to.equal(className) end) @@ -58,7 +58,7 @@ return function() it("should contain the host kind", function() local wrapper = shallow(createElement(SomeComponent)) - local result = Snapshot.type(wrapper.type) + local result = Snapshot.type(wrapper.component) expect(result.kind).to.equal(ElementKind.Function) end) @@ -75,7 +75,7 @@ return function() it("should contain the host kind", function() local wrapper = shallow(createElement(SomeComponent)) - local result = Snapshot.type(wrapper.type) + local result = Snapshot.type(wrapper.component) expect(result.kind).to.equal(ElementKind.Stateful) end) @@ -83,7 +83,7 @@ return function() it("should contain the component name", function() local wrapper = shallow(createElement(SomeComponent)) - local result = Snapshot.type(wrapper.type) + local result = Snapshot.type(wrapper.component) expect(result.componentName).to.equal(componentName) end) From aec002f702651b9ee1dbefeb00aeee316b095e67 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 21 Aug 2019 12:42:14 -0700 Subject: [PATCH 61/65] Fix typos and unused parameter --- src/VirtualTree.lua | 4 +-- .../Serialize/IndentedOutput.spec.lua | 2 +- src/shallow/Snapshot/Serialize/Serializer.lua | 10 ++++---- .../Snapshot/Serialize/Serializer.spec.lua | 25 +++++++++++++++++-- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua index a788690c..cc255dc8 100644 --- a/src/VirtualTree.lua +++ b/src/VirtualTree.lua @@ -6,7 +6,7 @@ local Type = require(script.Parent.Type) local config = require(script.Parent.GlobalConfig).get() -local DEFAULT_RENDERER = createReconciler(RobloxRenderer) +local DEFAULT_RECONCILER = createReconciler(RobloxRenderer) local InternalData = Symbol.named("InternalData") @@ -18,7 +18,7 @@ function VirtualTree.mount(element, options) options = options or {} local hostParent = options.hostParent local hostKey = options.hostKey or "RoactTree" - local reconciler = options.reconciler or DEFAULT_RENDERER + local reconciler = options.reconciler or DEFAULT_RECONCILER if config.typeChecks then assert(Type.of(element) == Type.Element, "Expected arg #1 to be of type Element") diff --git a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua index 1c14ee63..6d84903f 100644 --- a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua +++ b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua @@ -72,7 +72,7 @@ return function() end) describe("popAndWrite", function() - it("should write the line and push", function() + it("should pop and write the line", function() local output = IndentedOutput.new() output:writeAndPush("foo") diff --git a/src/shallow/Snapshot/Serialize/Serializer.lua b/src/shallow/Snapshot/Serialize/Serializer.lua index be2eb3f1..7540c1bf 100644 --- a/src/shallow/Snapshot/Serialize/Serializer.lua +++ b/src/shallow/Snapshot/Serialize/Serializer.lua @@ -210,10 +210,10 @@ function Serializer.children(children, output) output:popAndWrite("},") end -function Serializer.snapshotDataContent(snapshotData, output, skipHostKey) +function Serializer.snapshotDataContent(snapshotData, output, includeHostKey) Serializer.type(snapshotData.type, output) - if not skipHostKey then + if includeHostKey then output:write("hostKey = %q,", snapshotData.hostKey) end @@ -221,9 +221,9 @@ function Serializer.snapshotDataContent(snapshotData, output, skipHostKey) Serializer.children(snapshotData.children, output) end -function Serializer.snapshotData(snapshotData, output, skipHostKey) +function Serializer.snapshotData(snapshotData, output) output:writeAndPush("{") - Serializer.snapshotDataContent(snapshotData, output) + Serializer.snapshotDataContent(snapshotData, output, true) output:popAndWrite("},") end @@ -236,7 +236,7 @@ function Serializer.firstSnapshotData(snapshotData) output:write("") output:writeAndPush("return {") - Serializer.snapshotDataContent(snapshotData, output, true) + Serializer.snapshotDataContent(snapshotData, output, false) output:popAndWrite("}") output:popAndWrite("end") diff --git a/src/shallow/Snapshot/Serialize/Serializer.spec.lua b/src/shallow/Snapshot/Serialize/Serializer.spec.lua index b4963477..8d657326 100644 --- a/src/shallow/Snapshot/Serialize/Serializer.spec.lua +++ b/src/shallow/Snapshot/Serialize/Serializer.spec.lua @@ -330,7 +330,7 @@ return function() children = {}, } local output = IndentedOutput.new() - Serializer.snapshotDataContent(snapshotData, output) + Serializer.snapshotDataContent(snapshotData, output, true) expect(output:join()).to.equal( "type = {\n" @@ -341,6 +341,27 @@ return function() .. "children = {}," ) end) + + it("should not include the host key", function() + local snapshotData = { + type = { + kind = ElementKind.Function, + }, + hostKey = "HostKey", + props = {}, + children = {}, + } + local output = IndentedOutput.new() + Serializer.snapshotDataContent(snapshotData, output, false) + + expect(output:join()).to.equal( + "type = {\n" + .. " kind = ElementKind.Function,\n" + .. "},\n" + .. "props = {},\n" + .. "children = {}," + ) + end) end) describe("snapshotData", function() @@ -355,7 +376,7 @@ return function() } local contentOutput = IndentedOutput.new() contentOutput:push() - Serializer.snapshotDataContent(snapshotData, contentOutput) + Serializer.snapshotDataContent(snapshotData, contentOutput, true) local output = IndentedOutput.new() Serializer.snapshotData(snapshotData, output) From 4a830852235d14f8f24b98e004ad9795c862b900 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 21 Aug 2019 13:37:56 -0700 Subject: [PATCH 62/65] Refactor Virtual.mount --- src/Component.spec/setState.spec.lua | 2 +- src/PureComponent.spec.lua | 12 +++--------- src/VirtualTree.lua | 9 ++++++++- src/VirtualTree.spec.lua | 20 +++++++++----------- src/createReconcilerCompat.spec.lua | 2 +- src/init.lua | 9 +-------- 6 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/Component.spec/setState.spec.lua b/src/Component.spec/setState.spec.lua index 6526054c..b6b6865b 100644 --- a/src/Component.spec/setState.spec.lua +++ b/src/Component.spec/setState.spec.lua @@ -11,7 +11,7 @@ return function() local noopReconciler = createReconciler(NoopRenderer) local function mountWithNoop(element, hostParent, hostKey) - return VirtualTree.mount(element, { + return VirtualTree.mountWithOptions(element, { hostParent = hostParent, hostKey = hostKey, reconciler = noopReconciler diff --git a/src/PureComponent.spec.lua b/src/PureComponent.spec.lua index 31f7fc42..0ec17d0c 100644 --- a/src/PureComponent.spec.lua +++ b/src/PureComponent.spec.lua @@ -8,14 +8,6 @@ return function() local noopReconciler = createReconciler(NoopRenderer) - local function mountWithNoop(element, hostParent, hostKey) - return VirtualTree.mount(element, { - hostParent = hostParent, - hostKey = hostKey, - reconciler = noopReconciler - }) - end - it("should be extendable", function() local MyComponent = PureComponent:extend("MyComponent") @@ -59,7 +51,9 @@ return function() end local element = createElement(PureContainer) - local tree = mountWithNoop(element, nil, "PureComponent Tree") + local tree = VirtualTree.mountWithOptions(element, { + reconciler = noopReconciler + }) expect(updateCount).to.equal(0) diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua index cc255dc8..17ce60bd 100644 --- a/src/VirtualTree.lua +++ b/src/VirtualTree.lua @@ -14,7 +14,7 @@ local VirtualTree = {} local VirtualTreePublic = {} VirtualTreePublic.__index = VirtualTreePublic -function VirtualTree.mount(element, options) +function VirtualTree.mountWithOptions(element, options) options = options or {} local hostParent = options.hostParent local hostKey = options.hostKey or "RoactTree" @@ -41,6 +41,13 @@ function VirtualTree.mount(element, options) return tree end +function VirtualTree.mount(element, hostParent, hostKey) + return VirtualTree.mountWithOptions(element, { + hostParent = hostParent, + hostKey = hostKey, + }) +end + function VirtualTree.update(tree, newElement) local internalData = tree[InternalData] diff --git a/src/VirtualTree.spec.lua b/src/VirtualTree.spec.lua index 8e4329a5..964e4258 100644 --- a/src/VirtualTree.spec.lua +++ b/src/VirtualTree.spec.lua @@ -6,17 +6,11 @@ return function() local noopReconciler = createReconciler(NoopRenderer) - local function mountWithNoop(element, hostParent, hostKey) - return VirtualTree.mount(element, { - hostParent = hostParent, - hostKey = hostKey, - reconciler = noopReconciler - }) - end - describe("tree operations", function() it("should mount and unmount", function() - local tree = mountWithNoop(createElement("StringValue")) + local tree = VirtualTree.mountWithOptions(createElement("StringValue"), { + reconciler = noopReconciler, + }) expect(tree).to.be.ok() @@ -24,7 +18,9 @@ return function() end) it("should mount, update, and unmount", function() - local tree = mountWithNoop(createElement("StringValue")) + local tree = VirtualTree.mountWithOptions(createElement("StringValue"), { + reconciler = noopReconciler, + }) expect(tree).to.be.ok() @@ -36,7 +32,9 @@ return function() describe("getShallowWrapper", function() it("should return a shallow wrapper", function() - local tree = VirtualTree.mount(createElement("StringValue")) + local tree = VirtualTree.mountWithOptions(createElement("StringValue"), { + reconciler = noopReconciler, + }) expect(tree).to.be.ok() diff --git a/src/createReconcilerCompat.spec.lua b/src/createReconcilerCompat.spec.lua index 33a9d156..4a45bceb 100644 --- a/src/createReconcilerCompat.spec.lua +++ b/src/createReconcilerCompat.spec.lua @@ -10,7 +10,7 @@ return function() local noopReconciler = createReconciler(NoopRenderer) local function mountWithNoop(element, hostParent, hostKey) - return VirtualTree.mount(element, { + return VirtualTree.mountWithOptions(element, { hostParent = hostParent, hostKey = hostKey, reconciler = noopReconciler diff --git a/src/init.lua b/src/init.lua index 93e6ea4a..99f41262 100644 --- a/src/init.lua +++ b/src/init.lua @@ -8,13 +8,6 @@ local strict = require(script.strict) local Binding = require(script.Binding) local VirtualTree = require(script.VirtualTree) -local function mount(element, hostParent, hostKey) - return VirtualTree.mount(element, { - hostParent = hostParent, - hostKey = hostKey, - }) -end - local reconcilerCompat = createReconcilerCompat(VirtualTree) local Roact = strict { @@ -34,7 +27,7 @@ local Roact = strict { Event = require(script.PropMarkers.Event), Ref = require(script.PropMarkers.Ref), - mount = mount, + mount = VirtualTree.mount, unmount = VirtualTree.unmount, update = VirtualTree.update, From 01529badd78416e121740b848d13c9c2bd3e8d54 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Wed, 21 Aug 2019 14:06:26 -0700 Subject: [PATCH 63/65] Simplify IndentedOutput --- src/shallow/Snapshot/Serialize/IndentedOutput.lua | 14 ++++---------- .../Snapshot/Serialize/IndentedOutput.spec.lua | 12 ++++++------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/shallow/Snapshot/Serialize/IndentedOutput.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.lua index 0d33c896..4dd650ed 100644 --- a/src/shallow/Snapshot/Serialize/IndentedOutput.lua +++ b/src/shallow/Snapshot/Serialize/IndentedOutput.lua @@ -3,17 +3,13 @@ local IndentedOutputMetatable = { __index = IndentedOutput, } -function IndentedOutput.new(indentation, removeTrailingWhitespaces) +function IndentedOutput.new(indentation) indentation = indentation or 2 - if removeTrailingWhitespaces == nil then - removeTrailingWhitespaces = true - end local output = { _level = 0, _indentation = (" "):rep(indentation), _lines = {}, - _removeTrailingWhitespaces = removeTrailingWhitespaces, } setmetatable(output, IndentedOutputMetatable) @@ -26,7 +22,7 @@ function IndentedOutput:write(line, ...) line = line:format(...) end - if self._removeTrailingWhitespaces and line == "" then + if line == "" then table.insert(self._lines, line) else table.insert(self._lines, ("%s%s"):format(self._indentation:rep(self._level), line)) @@ -51,10 +47,8 @@ function IndentedOutput:popAndWrite(...) self:write(...) end -function IndentedOutput:join(separator) - separator = separator or "\n" - - return table.concat(self._lines, separator) +function IndentedOutput:join() + return table.concat(self._lines, "\n") end return IndentedOutput diff --git a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua index 6d84903f..66886c5d 100644 --- a/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua +++ b/src/shallow/Snapshot/Serialize/IndentedOutput.spec.lua @@ -10,19 +10,19 @@ return function() expect(output:join()).to.equal("foo\nbar") end) + end) - it("should concat the lines with the given string", function() + describe("write", function() + it("should preceed the line with the current indentation level", function() local output = IndentedOutput.new() + output:push() output:write("foo") - output:write("bar") - expect(output:join("-")).to.equal("foo-bar") + expect(output:join()).to.equal(" foo") end) - end) - describe("write", function() - it("should remove trailing whitespaces", function() + it("should not write indentation spaces when line is empty", function() local output = IndentedOutput.new() output:push() From 9f366de5ab9d5433fbfca7255a7b54289838c257 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 22 Aug 2019 10:29:06 -0700 Subject: [PATCH 64/65] Change getShallowWrapper parameters --- docs/advanced/shallow-rendering.md | 16 +++------ docs/api-reference.md | 11 ++---- src/VirtualTree.lua | 4 +-- src/shallow/ShallowWrapper.spec.lua | 40 +++++++-------------- src/shallow/init.lua | 10 +++--- src/shallow/validateShallowOptions.lua | 30 ---------------- src/shallow/validateShallowOptions.spec.lua | 37 ------------------- 7 files changed, 25 insertions(+), 123 deletions(-) delete mode 100644 src/shallow/validateShallowOptions.lua delete mode 100644 src/shallow/validateShallowOptions.spec.lua diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index a577195a..4aa94885 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -173,7 +173,7 @@ Here are examples of this kind of script written in Lua ([link](../scripts/sync- Sometimes during testing, components need to be wrapped into other components that provides some of their props. When comes the time to make a snapshot of that components, it's preferable to snapshot it without the wrapping component. The main reason for this is to avoid snapshot test failure when there is a change in the wrapping component that does not affect the wrapped component. -In the case where the component is wrapped in a host component (a component that render a Roblox instance), use ShallowWrapper's [`find`](/api-reference/#find) or [`findUnique`](/api-reference/#findunique) methods to get to your child component. Otherwise, increase the depth option to get deep enough so that the parent component is consumed. +In the case where the component is wrapped in a host component (a component that render a Roblox instance), use ShallowWrapper's [`find`](/api-reference/#find) or [`findUnique`](/api-reference/#findunique) methods to get to your child component. Otherwise, increase the depth parameter to get deep enough so that the parent component is consumed. !!! Note When making snapshot tests, depth is generally used more often than `find` or `findUnique`. Components may have multiple layer of elements nested: that will require adjusting the depth to render at least until the last element. The ShallowWrapper's methods that access its children are more used when working on generic components. These generic components may be a bit harder or tricky to test because they depend on other concrete components that get wrappred into them. @@ -264,9 +264,7 @@ local element = Roact.createElement(CoolComponent) local tree = Roact.mount(element) -local wrapper = tree:getShallowWrapper({ - depth = 2, -}) +local wrapper = tree:getShallowWrapper(2) wrapper:matchSnapshot("CoolComponent") ``` @@ -314,9 +312,7 @@ local element = Roact.createElement(CoolComponent) local tree = Roact.mount(element) -local wrapper = tree:getShallowWrapper({ - depth = 2, -}) +local wrapper = tree:getShallowWrapper(2) local coolWrapper = wrapper:findUnique() @@ -420,15 +416,13 @@ And the generated snapshot looks like this: When writing the test to snapshot `CoolComponent`, you will want to use to avoid having details of the `StyleConsumer`. The reason is that if `StyleConsumer` changes, the test may fail without really changing the important part of our snapshot, which is the information about `CoolComponent`. -Also, notice that there is no information about the `TextLabel`, because the method `getShallowWrapper` only returns a shallow version of the mounted tree. If we want to have access to the next depth of the tree, we need to pass the option `depth = 2`. +Also, notice that there is no information about the `TextLabel`, because the method `getShallowWrapper` only returns a shallow version of the mounted tree. If we want to have access to the next depth of the tree, we need to pass 2 to the depth parameter. ```lua local element = Roact.createElement(CoolComponent) local tree = Roact.mount(element) -local wrapper = tree:getShallowWrapper({ - depth = 2, -}) +local wrapper = tree:getShallowWrapper(2) wrapper:matchSnapshot("CoolSnapshot") ``` diff --git a/docs/api-reference.md b/docs/api-reference.md index 12b4c9fc..9434db24 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -738,13 +738,6 @@ Returns the string source of the snapshot. Useful for debugging purposes. #### getShallowWrapper ``` -getShallowWrapper(options) -> ShallowWrapper +getShallowWrapper(depth: number = 1) -> ShallowWrapper ``` -Options: -```lua -{ - depth: number -- default to 1 -} -``` - -Wraps the current tree into a ShallowWrapper. +Wraps the current tree into a ShallowWrapper. The depth parameter is optional and 1 by default. For more information about depth, visit the [shallow rendering page](/advanced/shallow-rendering#example-with-depth). diff --git a/src/VirtualTree.lua b/src/VirtualTree.lua index 17ce60bd..1cb4e262 100644 --- a/src/VirtualTree.lua +++ b/src/VirtualTree.lua @@ -81,13 +81,13 @@ function VirtualTree.unmount(tree) end end -function VirtualTreePublic:getShallowWrapper(options) +function VirtualTreePublic:getShallowWrapper(depth) assert(Type.of(self) == Type.VirtualTree, "Expected method getShallowWrapper to be called with `:`") local internalData = self[InternalData] local rootNode = internalData.rootNode - return shallow(rootNode, options) + return shallow(rootNode, depth) end return VirtualTree \ No newline at end of file diff --git a/src/shallow/ShallowWrapper.spec.lua b/src/shallow/ShallowWrapper.spec.lua index 96477392..8fd7a314 100644 --- a/src/shallow/ShallowWrapper.spec.lua +++ b/src/shallow/ShallowWrapper.spec.lua @@ -12,15 +12,13 @@ return function() local robloxReconciler = createReconciler(RobloxRenderer) - local function shallow(element, options) - options = options or {} - local maxDepth = options.depth or 1 - local hostKey = options.hostKey or "ShallowTree" - local hostParent = options.hostParent or Instance.new("Folder") + local function shallow(element, depth) + depth = depth or 1 + local hostParent = Instance.new("Folder") - local virtualNode = robloxReconciler.mountVirtualNode(element, hostParent, hostKey) + local virtualNode = robloxReconciler.mountVirtualNode(element, hostParent, "ShallowTree") - return ShallowWrapper.new(virtualNode, maxDepth) + return ShallowWrapper.new(virtualNode, depth) end describe("single host element", function() @@ -109,9 +107,7 @@ return function() it("should unwrap function components when depth has not exceeded", function() local element = createElement(Component) - local result = shallow(element, { - depth = 3, - }) + local result = shallow(element, 3) expect(result.component).to.equal(unwrappedClassName) end) @@ -119,9 +115,7 @@ return function() it("should stop unwrapping function components when depth has exceeded", function() local element = createElement(Component) - local result = shallow(element, { - depth = 2, - }) + local result = shallow(element, 2) expect(result.component).to.equal(A) end) @@ -129,9 +123,7 @@ return function() it("should not unwrap the element when depth is zero", function() local element = createElement(Component) - local result = shallow(element, { - depth = 0, - }) + local result = shallow(element, 0) expect(result.component).to.equal(Component) end) @@ -139,9 +131,7 @@ return function() it("should not unwrap children when depth is one", function() local element = createElement(ComponentWithChildren) - local result = shallow(element, { - depth = 1, - }) + local result = shallow(element) local childA = result:find({ component = A, @@ -157,9 +147,7 @@ return function() it("should unwrap children when depth is two", function() local element = createElement(ComponentWithChildren) - local result = shallow(element, { - depth = 2, - }) + local result = shallow(element, 2) local hostChild = result:find({ component = unwrappedClassName, @@ -175,9 +163,7 @@ return function() it("should not include any children when depth is zero", function() local element = createElement(ComponentWithChildren) - local result = shallow(element, { - depth = 0, - }) + local result = shallow(element, 0) expect(#result.children).to.equal(0) end) @@ -191,9 +177,7 @@ return function() local element = createElement(ParentComponent) - local result = shallow(element, { - depth = 1, - }) + local result = shallow(element, 1) expect(#result.children).to.equal(1) diff --git a/src/shallow/init.lua b/src/shallow/init.lua index 48386399..a137527c 100644 --- a/src/shallow/init.lua +++ b/src/shallow/init.lua @@ -1,15 +1,13 @@ local Type = require(script.Parent.Type) local ShallowWrapper = require(script.ShallowWrapper) -local validateShallowOptions = require(script.validateShallowOptions) -local function shallow(rootNode, options) +local function shallow(rootNode, depth) assert(Type.of(rootNode) == Type.VirtualNode, "Expected arg #1 to be a VirtualNode") - assert(validateShallowOptions(options)) + assert(depth == nil or type(depth) == "number", "Expected arg #2 to be a number") - options = options or {} - local maxDepth = options.depth or 1 + depth = depth or 1 - return ShallowWrapper.new(rootNode, maxDepth) + return ShallowWrapper.new(rootNode, depth) end return shallow \ No newline at end of file diff --git a/src/shallow/validateShallowOptions.lua b/src/shallow/validateShallowOptions.lua deleted file mode 100644 index eaecb9cf..00000000 --- a/src/shallow/validateShallowOptions.lua +++ /dev/null @@ -1,30 +0,0 @@ -local optionsTypes = { - depth = "number", -} - -local function validateShallowOptions(options) - if options == nil then - return true - end - - for key, value in pairs(options) do - local expectType = optionsTypes[key] - - if expectType == nil then - return false, ("unexpected option field %q (with value of %s)"):format( - tostring(key), - tostring(value) - ) - elseif typeof(value) ~= expectType then - return false, ("unexpected option type for %q (expected %s but got %s)"):format( - tostring(key), - expectType, - typeof(value) - ) - end - end - - return true -end - -return validateShallowOptions \ No newline at end of file diff --git a/src/shallow/validateShallowOptions.spec.lua b/src/shallow/validateShallowOptions.spec.lua deleted file mode 100644 index 69670471..00000000 --- a/src/shallow/validateShallowOptions.spec.lua +++ /dev/null @@ -1,37 +0,0 @@ -return function() - local validateShallowOptions = require(script.Parent.validateShallowOptions) - - it("should return true given nil", function() - expect(validateShallowOptions(nil)).to.equal(true) - end) - - it("should return true given an empty table", function() - expect(validateShallowOptions({})).to.equal(true) - end) - - it("should return true if the key's value match the expected type", function() - local success = validateShallowOptions({ - depth = 1, - }) - - expect(success).to.equal(true) - end) - - it("should return false if a key is not expected", function() - local success, message = validateShallowOptions({ - foo = 1, - }) - - expect(success).to.equal(false) - expect(message).to.be.a("string") - end) - - it("should return false if an expected value has not the correct type", function() - local success, message = validateShallowOptions({ - depth = "foo", - }) - - expect(success).to.equal(false) - expect(message).to.be.a("string") - end) -end \ No newline at end of file From 780fc4f7ac580542fb5145cc6e6785c1cb97a642 Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Thu, 22 Aug 2019 10:31:50 -0700 Subject: [PATCH 65/65] Fix shallow rendering docs typo --- docs/advanced/shallow-rendering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/shallow-rendering.md b/docs/advanced/shallow-rendering.md index 4aa94885..12a067d7 100644 --- a/docs/advanced/shallow-rendering.md +++ b/docs/advanced/shallow-rendering.md @@ -416,7 +416,7 @@ And the generated snapshot looks like this: When writing the test to snapshot `CoolComponent`, you will want to use to avoid having details of the `StyleConsumer`. The reason is that if `StyleConsumer` changes, the test may fail without really changing the important part of our snapshot, which is the information about `CoolComponent`. -Also, notice that there is no information about the `TextLabel`, because the method `getShallowWrapper` only returns a shallow version of the mounted tree. If we want to have access to the next depth of the tree, we need to pass 2 to the depth parameter. +Also, notice that there is no information about the `TextLabel`, because the method `getShallowWrapper` only returns a shallow version of the mounted tree. If we want to have access to the next depth of the tree, we need to pass 2 as the depth parameter. ```lua local element = Roact.createElement(CoolComponent)