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 var mountedChildren = [ComponentWrapper<R>]()
|
||||||
private let type: AnyCompositeComponent.Type
|
private let type: AnyCompositeComponent.Type
|
||||||
private let parentTarget: R.Target
|
private let parentTarget: R.Target
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in New Issue