Add tests for updates/umounts, optimise reconciler

This commit is contained in:
Max Desiatov 2018-12-29 16:49:31 +00:00
parent 5de0931849
commit cb1e0696cf
No known key found for this signature in database
GPG Key ID: FE08EBF9CF58CBA2
6 changed files with 143 additions and 22 deletions

View File

@ -14,7 +14,17 @@ extension CompositeComponent {
} }
} }
final class CompositeComponentWrapper<R: Renderer>: ComponentWrapper<R> { final class CompositeComponentWrapper<R: Renderer>: ComponentWrapper<R>,
Hashable {
static func ==(lhs: CompositeComponentWrapper<R>,
rhs: CompositeComponentWrapper<R>) -> Bool {
return lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
private var mountedChildren = [ComponentWrapper<R>]() private var mountedChildren = [ComponentWrapper<R>]()
private let type: AnyCompositeComponent.Type private let type: AnyCompositeComponent.Type
private let parentTarget: R.Target private let parentTarget: R.Target

View File

@ -76,20 +76,20 @@ final class HostComponentWrapper<R: Renderer>: ComponentWrapper<R> {
var newChildren = [ComponentWrapper<R>]() var newChildren = [ComponentWrapper<R>]()
while let child = mountedChildren.first, let node = nodes.first { while let child = mountedChildren.first, let node = nodes.first {
let newChild: ComponentWrapper<R>
if node.key != nil, if node.key != nil,
node.type == mountedChildren[0].node.type, node.type == mountedChildren[0].node.type,
node.key == child.node.key { node.key == child.node.key {
child.node = node child.node = node
child.update(with: reconciler) child.update(with: reconciler)
newChildren.append(child) newChild = child
mountedChildren.removeFirst()
} else { } else {
child.unmount(with: reconciler) child.unmount(with: reconciler)
let newChild: ComponentWrapper<R> = newChild = node.makeComponentWrapper(target)
node.makeComponentWrapper(target)
newChild.mount(with: reconciler) newChild.mount(with: reconciler)
newChildren.append(newChild)
} }
newChildren.append(newChild)
mountedChildren.removeFirst()
nodes.removeFirst() nodes.removeFirst()
} }

View File

@ -8,7 +8,7 @@
import Dispatch import Dispatch
public final class StackReconciler<R: Renderer> { public final class StackReconciler<R: Renderer> {
private var queuedState = [(CompositeComponentWrapper<R>, String, Any)]() private var queuedRerenders = Set<CompositeComponentWrapper<R>>()
public let rootTarget: R.Target public let rootTarget: R.Target
private let rootComponent: ComponentWrapper<R> private let rootComponent: ComponentWrapper<R>
@ -26,9 +26,10 @@ public final class StackReconciler<R: Renderer> {
func queue(state: Any, func queue(state: Any,
for component: CompositeComponentWrapper<R>, for component: CompositeComponentWrapper<R>,
id: String) { id: String) {
let scheduleReconcile = queuedState.isEmpty let scheduleReconcile = queuedRerenders.isEmpty
queuedState.append((component, id, state)) component.state[id] = state
queuedRerenders.insert(component)
guard scheduleReconcile else { return } guard scheduleReconcile else { return }
@ -38,10 +39,10 @@ public final class StackReconciler<R: Renderer> {
} }
private func updateStateAndReconcile() { private func updateStateAndReconcile() {
for (component, id, state) in queuedState { for component in queuedRerenders {
component.state[id] = state
component.update(with: self) component.update(with: self)
} }
queuedRerenders.removeAll()
} }
} }

View File

@ -15,8 +15,11 @@ public class TestRenderer: Renderer {
} }
public init(_ node: Node) { public init(_ node: Node) {
let root = TestView(component: View.self,
props: AnyEquatable(Null()),
children: AnyEquatable(Null()))
reconciler = StackReconciler(node: node, reconciler = StackReconciler(node: node,
target: TestView(props: AnyEquatable(Null())), target: root,
renderer: self) renderer: self)
} }
@ -24,7 +27,9 @@ public class TestRenderer: Renderer {
with component: AnyHostComponent.Type, with component: AnyHostComponent.Type,
props: AnyEquatable, props: AnyEquatable,
children: AnyEquatable) -> TestView? { children: AnyEquatable) -> TestView? {
let result = TestView(props: props) let result = TestView(component: component,
props: props,
children: children)
parent.add(subview: result) parent.add(subview: result)
return result return result
@ -35,6 +40,7 @@ public class TestRenderer: Renderer {
props: AnyEquatable, props: AnyEquatable,
children: AnyEquatable) { children: AnyEquatable) {
target.props = props target.props = props
target.children = children
} }
public func unmount(target: TestView, with component: AnyHostComponent.Type) { public func unmount(target: TestView, with component: AnyHostComponent.Type) {

View File

@ -17,13 +17,24 @@ public final class TestView {
/// Props assigned to this test view. /// Props assigned to this test view.
public internal(set) var props: AnyEquatable public internal(set) var props: AnyEquatable
/// Children assigned to this test view.
public internal(set) var children: AnyEquatable
/// Component that renders to this test view as a target
public let component: AnyHostComponent.Type
/// Parent `TestView` instance that owns this instance as a child
private weak var parent: TestView? private weak var parent: TestView?
/** Initialize a new test view. /** Initialize a new test view.
- parameter props: base component props to initialize the test view - parameter props: base component props to initialize the test view
*/ */
init(props: AnyEquatable) { init(component: AnyHostComponent.Type,
props: AnyEquatable,
children: AnyEquatable) {
self.component = component
self.props = props self.props = props
self.children = children
} }
/** Add a subview to this test view. /** Add a subview to this test view.

View File

@ -20,12 +20,15 @@ struct Counter: LeafComponent {
setCount(count + 1) setCount(count + 1)
} }
let children = count < 44 ? [
Button.node(.init(handlers: [.touchUpInside: handler]), "Increment"),
Label.node(Null(), "\(count)"),
] : []
return StackView.node(.init(axis: .vertical, return StackView.node(.init(axis: .vertical,
distribution: .fillEqually, distribution: .fillEqually,
frame: .zero), [ frame: .zero),
Button.node(.init(handlers: [.touchUpInside: handler]), "Increment"), children)
Label.node(Null(), "\(count)"),
])
} }
} }
@ -38,13 +41,103 @@ final class GluonTests: XCTestCase {
return return
} }
XCTAssert(root.component == View.self)
XCTAssertEqual(root.subviews.count, 1) XCTAssertEqual(root.subviews.count, 1)
XCTAssert(type(of: root.subviews[0].props.value) == StackViewProps.self) let stack = root.subviews[0]
XCTAssert(stack.component == StackView.self)
XCTAssert(type(of: stack.props.value) == StackView.Props.self)
XCTAssertEqual(stack.subviews.count, 2)
XCTAssert(stack.subviews[0].component == Button.self)
XCTAssert(stack.subviews[1].component == Label.self)
XCTAssertEqual(stack.subviews[1].children, AnyEquatable("42"))
} }
func testUpdate() {} func testUpdate() {
let renderer = TestRenderer(Counter.node(42))
func testUnmount() {} guard let root = renderer.rootTarget,
let props = root.subviews[0].subviews[0]
.props.value as? Button.Props else {
XCTAssert(false, "button component got wrong props types")
return
}
guard let handler = props.handlers[.touchUpInside]?.value else {
XCTAssert(false, "button component got no handler")
return
}
handler(())
let e = expectation(description: "rerender")
DispatchQueue.main.async {
XCTAssert(root.component == View.self)
XCTAssertEqual(root.subviews.count, 1)
let stack = root.subviews[0]
XCTAssert(stack.component == StackView.self)
XCTAssert(type(of: stack.props.value) == StackView.Props.self)
XCTAssertEqual(stack.subviews.count, 2)
XCTAssert(stack.subviews[0].component == Button.self)
XCTAssert(stack.subviews[1].component == Label.self)
XCTAssertEqual(stack.subviews[1].children, AnyEquatable("43"))
e.fulfill()
}
wait(for: [e], timeout: 1)
}
func testUnmount() {
let renderer = TestRenderer(Counter.node(42))
guard let root = renderer.rootTarget,
let props = root.subviews[0].subviews[0]
.props.value as? Button.Props else {
XCTAssert(false, "button component got wrong props types")
return
}
guard let handler = props.handlers[.touchUpInside]?.value else {
XCTAssert(false, "button component got no handler")
return
}
handler(())
let e = expectation(description: "rerender")
DispatchQueue.main.async {
// rerender completed here, schedule another one
guard let root = renderer.rootTarget,
let props = root.subviews[0].subviews[0]
.props.value as? Button.Props else {
XCTAssert(false, "button component got wrong props types")
return
}
guard let handler = props.handlers[.touchUpInside]?.value else {
XCTAssert(false, "button component got no handler")
return
}
handler(())
DispatchQueue.main.async {
XCTAssert(root.component == View.self)
XCTAssertEqual(root.subviews.count, 1)
let stack = root.subviews[0]
XCTAssert(stack.component == StackView.self)
XCTAssert(type(of: stack.props.value) == StackView.Props.self)
XCTAssertEqual(stack.subviews.count, 0)
e.fulfill()
}
}
wait(for: [e], timeout: 1)
}
static var allTests = [ static var allTests = [
("testMount", testMount), ("testMount", testMount),