-
Notifications
You must be signed in to change notification settings - Fork 143
Shallow wrapper #223
base: master
Are you sure you want to change the base?
Shallow wrapper #223
Changes from 15 commits
72c6921
2b7dde9
f57fa89
7ee2939
45b48da
8450335
a1f914f
17d8146
4183766
830c0b6
39eae67
16e1ba0
c81d22f
9b09be8
5944bf3
ae299a5
60b18f2
2773da6
edb1ca3
d73ebf1
f4844ac
a541ec4
8733795
1b8e6e8
0a187d9
a843b91
caca4ed
f1da1a2
8dcd096
6bd217e
90a0e40
10c3041
846ffd9
764c00a
a36239c
7ef84d3
dd5bc30
3230ad4
8986ce0
9324979
5efa96c
342bbec
750f669
12539ec
f1dac37
cfd055a
4bb8234
6d677c3
89b3a8e
7073e0d
2764cbc
3f44eab
0699e75
d5bcf88
90010ec
5b99367
27db6d9
1b1553d
ab59379
13edb92
ead931b
6dc2133
aec002f
4a83085
01529ba
9f366de
780fc4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
local Children = require(script.Parent.PropMarkers.Children) | ||
local ElementKind = require(script.Parent.ElementKind) | ||
local ElementUtils = require(script.Parent.ElementUtils) | ||
local snapshot = require(script.Parent.snapshot) | ||
|
||
local ShallowWrapper = {} | ||
local ShallowWrapperMetatable = { | ||
__index = ShallowWrapper, | ||
} | ||
|
||
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(('shallow wrapper does not support element of kind %q'):format(tostring(kind))) | ||
end | ||
end | ||
|
||
local function findNextVirtualNode(virtualNode, maxDepth) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for my own knowledge, what's the role of this function? If we call Shallow.new why can't we assume we passed in the next virtual node? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is the thing that gives the illusion of shallow rendering, I have not documented it a lot but if you take a look at the notes repo, checkout the tips page there is a example when you are using So I have written a lot of tests to verify the behavior of depth, it's a bit confusing honestly. I've changed the definition a few times before getting this version. It might be something worth reviewing together too if you want There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depth might be more intuitive if it still preserves the parent. I tried running some of the tests like stateful-component-children with a depth of 2, and I would have expected CoolComponent to still be in my snapshot, but get to see its children. |
||
local currentDepth = 0 | ||
local currentNode = virtualNode | ||
local nextNode = currentNode.children[ElementUtils.UseParentKey] | ||
|
||
while currentDepth < maxDepth and nextNode ~= nil do | ||
currentNode = nextNode | ||
nextNode = currentNode.children[ElementUtils.UseParentKey] | ||
currentDepth = currentDepth + 1 | ||
end | ||
|
||
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 | ||
|
||
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 | ||
getChildren(subVirtualNode, results, maxDepth) | ||
end | ||
else | ||
local childWrapper = ShallowWrapper.new( | ||
virtualNode, | ||
maxDepth | ||
) | ||
|
||
table.insert(results, childWrapper) | ||
end | ||
end | ||
|
||
local function filterProps(props) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it could be simpler to just not filter the props in the shallow at all There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want remove the Roact.Children key from the props when it's there. That is because it makes it easier to assert your props. Intuitively, it matches what you put in the props argument of Roact.createElement. |
||
if props[Children] == nil then | ||
return props | ||
end | ||
|
||
local filteredProps = {} | ||
|
||
for key, value in pairs(props) do | ||
if key ~= Children then | ||
filteredProps[key] = value | ||
end | ||
end | ||
|
||
return filteredProps | ||
end | ||
|
||
function ShallowWrapper.new(virtualNode, maxDepth) | ||
virtualNode = findNextVirtualNode(virtualNode, maxDepth) | ||
|
||
local wrapper = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to simplify this class, the wrapper could just be the node + the depth, and we could remove some of the mapping we are doing from the virtual node interface to the shallow |
||
_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, | ||
instance = virtualNode.hostObject, | ||
} | ||
|
||
return setmetatable(wrapper, ShallowWrapperMetatable) | ||
end | ||
|
||
function ShallowWrapper:childrenCount() | ||
local count = 0 | ||
|
||
for _, virtualNode in pairs(self._virtualNodeChildren) do | ||
local element = virtualNode.currentElement | ||
count = count + countChildrenOfElement(element) | ||
end | ||
|
||
return count | ||
end | ||
|
||
function ShallowWrapper:find(constraints) | ||
for constraint in pairs(constraints) do | ||
if not ContraintFunctions[constraint] then | ||
error(('unknown constraint %q'):format(constraint)) | ||
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:findUnique(constraints) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need convenience methods like this? I'm trying to imagine when developers will want to use this versus relying on snapshot matching. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We decided to keep this for cases where we want to remove some scaffolding injected by things like mock providers |
||
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 | ||
jeparlefrancais marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
|
||
local results = {} | ||
|
||
for _, childVirtualNode in pairs(self._virtualNodeChildren) do | ||
getChildren(childVirtualNode, results, self._childrenMaxDepth) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of using a non member function, this could be a bit simpler:
this would need the fragment check as well if we got rid of the other function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The thing I don't like with the member function is that I would have to copy back the elements from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perf is a good point, but I wonder if that concern goes away if we redefine depth such that we're building more deeply nested tables instead of coalescing the children at various levels of the tree to be the shallow children. |
||
end | ||
|
||
self._shallowChildren = results | ||
return results | ||
end | ||
|
||
function ShallowWrapper:toMatchSnapshot(identifier) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it makes sense to keep the idea of snapshots separate from the idea of shallows - this API can be the public function I was describing in another comment |
||
assert(typeof(identifier) == "string", "Snapshot identifier must be a string") | ||
|
||
local snapshotResult = snapshot(identifier, self) | ||
|
||
snapshotResult:match() | ||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reminder for us to document the types we support when we get to writing documentation for snapshot testing