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 let type: AnyCompositeComponent.Type
private let parentTarget: R.Target

View File

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

View File

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

View File

@ -15,8 +15,11 @@ public class TestRenderer: Renderer {
}
public init(_ node: Node) {
let root = TestView(component: View.self,
props: AnyEquatable(Null()),
children: AnyEquatable(Null()))
reconciler = StackReconciler(node: node,
target: TestView(props: AnyEquatable(Null())),
target: root,
renderer: self)
}
@ -24,7 +27,9 @@ public class TestRenderer: Renderer {
with component: AnyHostComponent.Type,
props: AnyEquatable,
children: AnyEquatable) -> TestView? {
let result = TestView(props: props)
let result = TestView(component: component,
props: props,
children: children)
parent.add(subview: result)
return result
@ -35,6 +40,7 @@ public class TestRenderer: Renderer {
props: AnyEquatable,
children: AnyEquatable) {
target.props = props
target.children = children
}
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.
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?
/** Initialize a new 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.children = children
}
/** Add a subview to this test view.

View File

@ -20,12 +20,15 @@ struct Counter: LeafComponent {
setCount(count + 1)
}
let children = count < 44 ? [
Button.node(.init(handlers: [.touchUpInside: handler]), "Increment"),
Label.node(Null(), "\(count)"),
] : []
return StackView.node(.init(axis: .vertical,
distribution: .fillEqually,
frame: .zero), [
Button.node(.init(handlers: [.touchUpInside: handler]), "Increment"),
Label.node(Null(), "\(count)"),
])
frame: .zero),
children)
}
}
@ -38,13 +41,103 @@ final class GluonTests: XCTestCase {
return
}
XCTAssert(root.component == View.self)
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 = [
("testMount", testMount),