Add tests for updates/umounts, optimise reconciler
This commit is contained in:
parent
5de0931849
commit
cb1e0696cf
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in New Issue