diff --git a/Sources/React/Element/Component.swift b/Sources/React/Element/Component.swift index 12134ab..7c9f2eb 100644 --- a/Sources/React/Element/Component.swift +++ b/Sources/React/Element/Component.swift @@ -6,24 +6,15 @@ public protocol Component: Element { var deps: Deps? { get } func render() -> Node - - static func _extractGhost(_ input: GhostInput) -> Ghost } extension Component { public var key: AnyHashable? { nil } public var deps: Deps? { nil } +} - static func extractGhostDefault(_ input: GhostInput) -> Ghost { - let hooks = extractHooks(input.component) - - return Ghost( - component: input.component, - hooks: hooks - ) - } - +enum Components { static func extractHooks(_ value: Any) -> [any _AnyHookWrapper] { var hooks: [any _AnyHookWrapper] = [] @@ -40,8 +31,4 @@ extension Component { return hooks } - - public static func _extractGhost(_ input: GhostInput) -> Ghost { - extractGhostDefault(input) - } } diff --git a/Sources/React/Element/Ghost.swift b/Sources/React/Element/Ghost.swift deleted file mode 100644 index 6746c90..0000000 --- a/Sources/React/Element/Ghost.swift +++ /dev/null @@ -1,28 +0,0 @@ -public struct GhostInput { - public init( - component: C - ) { - self.component = component - } - - public var component: C -} - -public struct Ghost { - public var component: any Component - - var hooks: [any _AnyHookWrapper] - var contextValue: (type: any ContextValue.Type, value: any ContextValue)? - - var states: [any _AnyStateHook] { - hooks.compactMap { $0 as? any _AnyStateHook } - } - - var contexts: [any _AnyContextHook] { - hooks.compactMap { $0 as? any _AnyContextHook } - } - - var effects: [any _AnyEffectHook] { - hooks.compactMap { $0 as? any _AnyEffectHook } - } -} diff --git a/Sources/React/Hooks/Context/ContextValueProvider.swift b/Sources/React/Hooks/Context/ContextValueProvider.swift index c27f674..16d8088 100644 --- a/Sources/React/Hooks/Context/ContextValueProvider.swift +++ b/Sources/React/Hooks/Context/ContextValueProvider.swift @@ -2,7 +2,7 @@ extension ContextValue { public typealias Provider = ContextValueProvider } -public struct ContextValueProvider: Component { +public struct ContextValueProvider: Component & _AnyContextValueProvider { public init( key: AnyHashable? = nil, value: Value, @@ -29,9 +29,11 @@ public struct ContextValueProvider: Component { children } - public static func _extractGhost(_ input: GhostInput) -> Ghost { - var ghost = extractGhostDefault(input) - ghost.contextValue = (type: Value.self, value: input.component.value) - return ghost - } + var _contextValueType: any ContextValue.Type { Value.self } + var _contextValue: any ContextValue { value } +} + +internal protocol _AnyContextValueProvider { + var _contextValueType: any ContextValue.Type { get } + var _contextValue: any ContextValue { get } } diff --git a/Sources/React/Hooks/Effect.swift b/Sources/React/Hooks/Effect.swift index 9df719a..83a88d2 100644 --- a/Sources/React/Hooks/Effect.swift +++ b/Sources/React/Hooks/Effect.swift @@ -56,6 +56,11 @@ public struct Effect: _AnyEffectHook { shouldExecute = false return Task(object: self, setup: setup) } + + func cleanupTask() -> Task? { + if cleanup == nil { return nil } + return Task(object: self, setup: nil) + } } final class Task { diff --git a/Sources/React/Image/Image.swift b/Sources/React/Image/Image.swift new file mode 100644 index 0000000..6b1a2e1 --- /dev/null +++ b/Sources/React/Image/Image.swift @@ -0,0 +1,5 @@ +public struct Image { + +} + + diff --git a/Sources/React/Renderer/Instance.swift b/Sources/React/Renderer/Instance.swift new file mode 100644 index 0000000..57a1ee8 --- /dev/null +++ b/Sources/React/Renderer/Instance.swift @@ -0,0 +1,104 @@ +import SRTDOM + +package final class Instance { + final class ListenerBridge { + init() {} + + var js: JSEventListener? + var swift: EventListener? + } + + init() { + + } + + weak var owner: VNode? + var hooks: [any _AnyHookWrapper] = [] + public var dom: JSNode? + var attributes: Attributes = [:] + var listeners: [String: ListenerBridge] = [:] + var contextValueHolder: ContextValueHolder? + var isDirty: Bool = false + + var contextHooks: [any _AnyContextHook] { + hooks.compactMap { $0 as? any _AnyContextHook } + } + + var stateHooks: [any _AnyStateHook] { + hooks.compactMap { $0 as? any _AnyStateHook } + } + + var effectHooks: [any _AnyEffectHook] { + hooks.compactMap { $0 as? any _AnyEffectHook } + } + + func renderDOMAttributes( + attributes newAttributes: Attributes + ) throws { + let dom = try (self.dom?.asHTMLElement()).unwrap("dom.asHTMLElement") + let oldAttributes = self.attributes + + for name in self.attributes.keys { + if newAttributes[name] == nil { + try dom.removeAttribute(name) + } + } + + for (name, newValue) in newAttributes { + if newValue != oldAttributes[name] { + try dom.setAttribute(name, newValue) + } + } + + self.attributes = newAttributes + } + + func renderDOMListeners( + listeners newListeners: EventListeners + ) throws { + let dom = try (self.dom?.asHTMLElement()).unwrap("dom.asHTMLElement") + + // コピー必要なんだっけ? + for (type, bridge) in Array(self.listeners) { + if bridge.swift != newListeners[type] { + if let js = bridge.js { + try dom.removeEventListener(type, js) + } + self.listeners[type] = nil + } + } + + for (type, newListener) in newListeners { + if let bridge = self.listeners[type] { + if newListener == bridge.swift { + continue + } + + bridge.swift = newListener + } else { + let bridge = ListenerBridge() + self.listeners[type] = bridge + + let js = JSEventListener { [weak bridge] (event) in + guard let bridge, let swift = bridge.swift else { return } + swift(event) + } + + bridge.js = js + bridge.swift = newListener + + try dom.addEventListener(type, js) + } + } + } + + func markDirty() { + isDirty = true + } + + func consumeDirty() -> Bool { + defer { isDirty = false } + return isDirty + } + +} diff --git a/Sources/React/Renderer/ReactRoot.swift b/Sources/React/Renderer/ReactRoot.swift index 2af6901..5929c6f 100644 --- a/Sources/React/Renderer/ReactRoot.swift +++ b/Sources/React/Renderer/ReactRoot.swift @@ -35,8 +35,8 @@ public final class ReactRoot { switch action { case .renderRoot(let node): try runRenderRoot(node: node) - case .update(let node): - try runUpdate(node: node) + case .update(let instance): + try runUpdate(instance: instance) case .effect(let effect): effect.run() } @@ -54,8 +54,8 @@ public final class ReactRoot { scheduler.resume() } - private func scheduleUpdate(node: VNode) { - scheduler.schedule(action: .update(node)) + private func scheduleUpdate(instance: Instance) { + scheduler.schedule(action: .update(instance)) } private func scheduleEffect(_ effect: Effect.Task) { @@ -65,7 +65,7 @@ public final class ReactRoot { } private func runRenderRoot(node: Node) throws { - let newRoot = Self.makeVNode(component: Fragment()) + let newRoot = VNode(component: Fragment()) let newChildren = Self.makeChildren(node: node) newRoot.appendChildren(newChildren) let oldChildren = root?.children ?? [] @@ -78,8 +78,9 @@ public final class ReactRoot { self.root = newRoot } - private func runUpdate(node oldTree: VNode) throws { - let newTree = VNode(ghost: oldTree.ghost) + private func runUpdate(instance: Instance) throws { + let oldTree = try instance.owner.unwrap("instance.owner") + let newTree = VNode(component: oldTree.component) let parent = try oldTree.parent.unwrap("oldTree.parent") let index = try parent.index(of: oldTree).unwrap("oldTree index") @@ -94,16 +95,9 @@ public final class ReactRoot { } } - private static func makeVNode(component: C) -> VNode { - let ghost = C._extractGhost( - .init(component: component) - ) - return VNode(ghost: ghost) - } - private static func makeChildren(node: Node) -> [VNode] { let nodes = Nodes.normalize(node: node) - return nodes.map { makeVNode(component: $0) } + return nodes.map { VNode(component: $0) } } private func withLocation(_ location: JSNodeLocationRight?, _ body: () throws -> Void) rethrows { @@ -150,8 +144,8 @@ public final class ReactRoot { } private func domLocation(of node: VNode) throws -> JSNodeLocationRight? { - let parent: JSNode = try node.parentTagNode?.dom.unwrap("dom") ?? self.dom.asNode() - let prev: JSNode? = try node.prevSiblingTagNode?.dom.unwrap("dom") + let parent: JSNode = try node.parentTagNode?.instance?.dom.unwrap("dom") ?? self.dom.asNode() + let prev: JSNode? = try node.prevSiblingTagNode?.instance?.dom.unwrap("dom") return JSNodeLocationRight(parent: parent, prev: prev) } @@ -162,180 +156,167 @@ public final class ReactRoot { ) throws { var doesRenderChildren = true - if let oldTree { - oldTree.new = .some(newTree) + if let newTree { + let instance = transferInstance(newTree: newTree, oldTree: oldTree) + let isFirst = oldTree == nil + + try preRender(tree: newTree, instance: instance, isFirst: isFirst) + + // short circuit + doesRenderChildren = instance.consumeDirty() || + isChanged(new: newTree, old: oldTree) } - if let newTree { - if let newTag = newTree.tagElement { - let dom: JSHTMLElement = if let oldTree { - try oldTree.domTag.unwrap("oldTree.domTag") - } else { - try document.createElement(newTag.tagName) - } + if doesRenderChildren { + try renderChildren(newTree: newTree, oldTree: oldTree) + } else if let newTree { + try skipRenderChildren(newTree: newTree, oldTree: oldTree, isMove: isMove) + } - let oldTag = oldTree?.tagElement - newTree.dom = dom.asNode() - newTree.listeners = oldTree?.listeners ?? [:] - newTag.ref?.value = dom - - try renderDOMAttributes( - dom: dom, - newAttributes: newTag.attributes, - oldAttributes: oldTag?.attributes ?? [:] - ) - try renderDOMEventListeners( - node: newTree, - dom: dom, - newListeners: newTag.listeners, - oldListeners: oldTag?.listeners ?? [:] - ) - } else if let text = newTree.textElement { - let dom: JSText = try { - if let oldTree { - let dom = try oldTree.domText.unwrap("oldTree.domText") - dom.data = text.value - return dom - } else { - return try document.createTextNode(text.value) - } - }() - - newTree.dom = dom.asNode() + if let newTree { + if let instance = newTree.instance { + try postRender(instance: instance) + } + } else if let oldTree { + if let instance = oldTree.instance { + try postRenderCleanup(instance: instance) } + } + } - if let location = currentLocation { - if let dom = newTree.dom { - if location != dom.locationRight { - try dom.remove() - try dom.insert(at: location) - } + private func transferInstance(newTree: VNode, oldTree: VNode?) -> Instance { + let oldInstance = oldTree?.instance + oldTree?.instance = nil + let instance = oldInstance ?? Instance() + newTree.instance = instance + return instance + } - currentLocation?.prev = dom - } - } + private func preRender(tree: VNode, instance: Instance, isFirst: Bool) throws { + try renderDOM(tree: tree, instance: instance) + try moveDOM(instance: instance) + try updateContextValue(tree: tree, instance: instance) + prepareHooks(component: tree.component, instance: instance, isFirst: isFirst) + subscribeHooks(instance: instance) + } - if let contextValue = newTree.ghost.contextValue { - let holder: ContextValueHolder = if let oldHolder = oldTree?.contextValueHolder, - oldHolder.type == contextValue.type - { - oldHolder - } else { - ContextValueHolder(type: contextValue.type) + private func renderDOM(tree: VNode, instance: Instance) throws { + switch tree.component { + case let newTag as HTMLElement: + let dom: JSHTMLElement = try { + if let dom = instance.dom?.asHTMLElement() { + return dom } - newTree.contextValueHolder = holder - holder.value = contextValue.value - } + let dom = try document.createElement(newTag.tagName) + instance.dom = dom.asNode() + return dom + }() - renderGhost(newTree: newTree, oldTree: oldTree) + newTag.ref?.value = dom - let updater = { [weak self, weak newTree] () -> Void in - guard let self, let newTree else { return } - newTree.markDirty() - self.scheduleUpdate(node: newTree) + try instance.renderDOMAttributes(attributes: newTag.attributes) + try instance.renderDOMListeners(listeners: newTag.listeners) + case let text as TextElement: + if let dom = instance.dom?.asText() { + dom.data = text.value + return } + let dom = try document.createTextNode(text.value) + instance.dom = dom.asNode() + default: break + } + } - for context in newTree.ghost.contexts { - if let holder = contextValueHolders[ObjectIdentifier(context.valueType)] { - let dsp = holder.emitter.on(handler: updater) - context.setHolder(holder, disposable: dsp) - } else { - context.setHolder(nil, disposable: nil) - } - } + private func moveDOM(instance: Instance) throws { + guard let location = currentLocation else { return } - for state in newTree.ghost.states { - state.setDidChange(updater) + if let dom = instance.dom { + if location != dom.locationRight { + try dom.remove() + try dom.insert(at: location) } - var isDirty = newTree.consumeDirty() + currentLocation?.prev = dom + } + } - if !isDirty { - if let newDeps = newTree.ghost.component.deps, - let oldDeps = oldTree?.ghost.component.deps, - newDeps == oldDeps - { - // same - } else { - isDirty = true - } - } + private func updateContextValue(tree: VNode, instance: Instance) throws { + guard let provider = tree.component as? any _AnyContextValueProvider else { return } - if isDirty { - let component = newTree.ghost.component + let type = provider._contextValueType + let value = provider._contextValue - willComponentRender?(component) - let node: Node = component.render() - didComponentRender?(component) + let holder: ContextValueHolder = { + if let holder = instance.contextValueHolder, + holder.type == type { return holder } - let newChildren = Self.makeChildren(node: node) - newTree.appendChildren(newChildren) - } else { - newTree.appendChildren(oldTree?.children ?? []) - doesRenderChildren = false - } - } + let holder = ContextValueHolder(type: type) + instance.contextValueHolder = holder + return holder + }() - if doesRenderChildren { - try renderChildren( - new: newTree?.children ?? [], - old: oldTree?.children ?? [], - parent: newTree?.dom, - contextValueHolder: newTree?.contextValueHolder - ) + holder.value = value + } + + private func prepareHooks(component: any Component, instance: Instance, isFirst: Bool) { + let hooks = Components.extractHooks(component) + + if isFirst { + for hook in hooks { + hook._prepareAny(object: nil) + } } else { - if let newTree, - newTree.dom == nil, - let _ = currentLocation - { - let doms = newTree.domChildren - if isMove { - for dom in doms { - try dom.remove() - } - for dom in doms { - try dom.insert(at: currentLocation!) - currentLocation!.prev = dom - } - } else { - if let dom = doms.last { - currentLocation!.prev = dom - } - } + for (new, old) in zip(hooks, instance.hooks) { + new._prepareAny(object: old.object) } } - if newTree == nil { - if let oldTree { - try oldTree.dom?.remove() + instance.hooks = hooks + } + + private func subscribeHooks(instance: Instance) { + let updater = { [weak self, weak instance] () -> Void in + guard let self, let instance else { return } + instance.markDirty() + self.scheduleUpdate(instance: instance) + } + + for context in instance.contextHooks { + if let holder = contextValueHolders[ObjectIdentifier(context.valueType)] { + let dsp = holder.emitter.on(handler: updater) + context.setHolder(holder, disposable: dsp) + } else { + context.setHolder(nil, disposable: nil) } } + for state in instance.stateHooks { + state.setDidChange(updater) + } + } - if let newTree { - for effect in newTree.ghost.effects { - let object = effect.effectObject - if let task = object.taskIfShouldExecute() { - scheduleEffect(task) - } - } - } else if let oldTree { - for effect in oldTree.ghost.effects { - let object = effect.effectObject - if let _ = object.cleanup { - let task = Effect.Task(object: object, setup: nil) - scheduleEffect(task) - } + private func isChanged(new: VNode, old: VNode?) -> Bool { + if let newDeps = new.component.deps, + newDeps == old?.component.deps { return false } + return true + } + + private func postRender(instance: Instance) throws { + for effect in instance.effectHooks { + if let task = effect.effectObject.taskIfShouldExecute() { + scheduleEffect(task) } } } - private func renderGhost(newTree: VNode, oldTree: VNode?) { - for (index, newHook) in newTree.ghost.hooks.enumerated() { - let oldHook = oldTree?.ghost.hooks[index] + private func postRenderCleanup(instance: Instance) throws { + try instance.dom?.remove() - newHook._prepareAny(object: oldHook?.object) + for effect in instance.effectHooks { + if let task = effect.effectObject.cleanupTask() { + scheduleEffect(task) + } } } @@ -345,7 +326,7 @@ public final class ReactRoot { // skip self var node: VNode? = node.parent while let n = node { - if let holder = n.contextValueHolder { + if let holder = n.instance?.contextValueHolder { let typeID = ObjectIdentifier(holder.type) if result[typeID] == nil { result[typeID] = holder @@ -358,6 +339,29 @@ public final class ReactRoot { return result } + private func renderChildren( + newTree: VNode?, + oldTree: VNode? + ) throws { + if let newTree { + let component = newTree.component + + willComponentRender?(component) + let node: Node = component.render() + didComponentRender?(component) + + let newChildren = Self.makeChildren(node: node) + newTree.appendChildren(newChildren) + } + + try renderChildren( + new: newTree?.children ?? [], + old: oldTree?.children ?? [], + parent: newTree?.instance?.dom, + contextValueHolder: newTree?.instance?.contextValueHolder + ) + } + private func renderChildren( new: [VNode], old: [VNode], @@ -415,59 +419,25 @@ public final class ReactRoot { nextIndex = newChildren.count } - private func renderDOMAttributes( - dom: JSHTMLElement, - newAttributes: Attributes, - oldAttributes: Attributes - ) throws { - for name in oldAttributes.keys { - if newAttributes[name] == nil { - try dom.removeAttribute(name) - } - } + private func skipRenderChildren(newTree: VNode, oldTree: VNode?, isMove: Bool) throws { + newTree.appendChildren(oldTree?.children ?? []) - for (name, newValue) in newAttributes { - if newValue != oldAttributes[name] { - try dom.setAttribute(name, newValue) - } - } - } + if let _ = newTree.instance?.dom { return } + guard let _ = currentLocation else { return } - private func renderDOMEventListeners( - node: VNode, - dom: JSHTMLElement, - newListeners: EventListeners, - oldListeners: EventListeners - ) throws { - for (type, oldListener) in oldListeners { - if oldListener != newListeners[type] { - if let bridge = node.listeners[type] { - if let js = bridge.js { - try dom.removeEventListener(type, js) - } - node.listeners[type] = nil - } - } - } - - for (type, newListener) in newListeners { - if newListener != oldListeners[type] { - if let bridge = node.listeners[type] { - bridge.swift = newListener - } else { - let bridge = VNode.ListenerBridge() - node.listeners[type] = bridge - - let js = JSEventListener { [weak bridge] (event) in - guard let bridge, let swift = bridge.swift else { return } - swift(event) - } + let doms = newTree.domChildren - bridge.js = js - bridge.swift = newListener - - try dom.addEventListener(type, js) - } + if isMove { + for dom in doms { + try dom.remove() + } + for dom in doms { + try dom.insert(at: currentLocation!) + currentLocation!.prev = dom + } + } else { + if let dom = doms.last { + currentLocation!.prev = dom } } } diff --git a/Sources/React/Renderer/Scheduler.swift b/Sources/React/Renderer/Scheduler.swift index b88973f..6c0528b 100644 --- a/Sources/React/Renderer/Scheduler.swift +++ b/Sources/React/Renderer/Scheduler.swift @@ -7,10 +7,10 @@ internal final class Scheduler { enum Action { case renderRoot(Node) - case update(VNode) + case update(Instance) case effect(Effect.Task) - var update: VNode? { + var update: Instance? { switch self { case .update(let x): x default: nil @@ -66,27 +66,13 @@ internal final class Scheduler { } updates.sort { (a, b) in - guard let a = a.update, let b = b.update else { return false } + guard let a = a.update?.owner, let b = b.update?.owner else { return false } return isLess(a, b) } actionQueue.replaceSubrange(.. Bool { a.equality == b.equality } @@ -56,15 +53,6 @@ package final class VNode: Hashable { hasher.combine(equality) } - public func markDirty() { - isDirty = true - } - - public func consumeDirty() -> Bool { - defer { isDirty = false } - return isDirty - } - public var parent: VNode? { _parent } public func removeFromParent() { @@ -123,23 +111,4 @@ package final class VNode: Hashable { public static func unknownNode(_ node: VNode) -> any Error { MessageError("unknown VNode: \(type(of: node))") } - - package static func tag( - _ name: String, - _ attributes: Attributes = [:], - _ children: [Node] = [] - ) -> VNode { - let tag = HTMLElement( - tagName: name, - attributes: attributes, - children: children - ) - return component(tag) - } - - package static func component(_ component: C) -> VNode { - let input = GhostInput(component: component) - let ghost = C._extractGhost(input) - return VNode(ghost: ghost) - } } diff --git a/Sources/React/VDOM/VNodeEx.swift b/Sources/React/VDOM/VNodeEx.swift index 65acedf..9cfe641 100644 --- a/Sources/React/VDOM/VNodeEx.swift +++ b/Sources/React/VDOM/VNodeEx.swift @@ -1,20 +1,12 @@ import SRTDOM extension VNode { - public var tagElement: HTMLElement? { - ghost.component as? HTMLElement + public var htmlElement: HTMLElement? { + component as? HTMLElement } - package var textElement: TextElement? { - ghost.component as? TextElement - } - - public var domTag: JSHTMLElement? { - dom?.asHTMLElement() - } - - public var domText: JSText? { - dom?.asText() + public var textElement: TextElement? { + component as? TextElement } public var parentTagNode: VNode? { @@ -25,7 +17,7 @@ extension VNode { return nil } - if node.tagElement != nil { + if node.htmlElement != nil { return node } @@ -48,7 +40,7 @@ extension VNode { if let found = node.find( direction: .left, predicate: { (node) in - node.tagElement != nil || + node.htmlElement != nil || node.textElement != nil } ) { @@ -56,7 +48,7 @@ extension VNode { } } - if parent.tagElement != nil { + if parent.htmlElement != nil { return nil } @@ -69,7 +61,7 @@ extension VNode { var doms: [JSNode] = [] walk { (node) in - if let dom = node.dom { + if let dom = node.instance?.dom { doms.append(dom) return .skipChildren } diff --git a/Tests/ReactTests/RefHookTests.swift b/Tests/ReactTests/RefHookTests.swift index 8cefc8e..485f65c 100644 --- a/Tests/ReactTests/RefHookTests.swift +++ b/Tests/ReactTests/RefHookTests.swift @@ -43,8 +43,8 @@ final class RefHookTests: XCTestCase { let btn: JSHTMLElement = try XCTUnwrap( root.root? - .find { $0.tagElement?.tagName == "button" }? - .dom?.asHTMLElement() + .find { $0.htmlElement?.tagName == "button" }? + .instance?.dom?.asHTMLElement() ) XCTAssertEqual(refs.count, 2) @@ -88,8 +88,8 @@ final class RefHookTests: XCTestCase { let btn: JSHTMLElement = try XCTUnwrap( root.root? - .find { $0.tagElement?.tagName == "button" }? - .dom?.asHTMLElement() + .find { $0.htmlElement?.tagName == "button" }? + .instance?.dom?.asHTMLElement() ) XCTAssertEqual(refs.count, 2) diff --git a/Tests/ReactTests/RenderPlanTests.swift b/Tests/ReactTests/RenderPlanTests.swift index 9ee73c1..4c420a9 100644 --- a/Tests/ReactTests/RenderPlanTests.swift +++ b/Tests/ReactTests/RenderPlanTests.swift @@ -57,8 +57,8 @@ final class RenderPlanTests: XCTestCase { let btn: JSHTMLElement = try XCTUnwrap( root.root? - .find { $0.tagElement?.tagName == "button" }? - .dom?.asHTMLElement() + .find { $0.htmlElement?.tagName == "button" }? + .instance?.dom?.asHTMLElement() ) try btn.click() @@ -148,8 +148,8 @@ final class RenderPlanTests: XCTestCase { let section: Section = try XCTUnwrap( root.root? - .find { $0.ghost.component is Section }? - .ghost.component as? Section + .find { $0.component is Section }? + .component as? Section ) XCTAssertEqual(evs, ["c", "s"]) diff --git a/Tests/ReactTests/RenderTests.swift b/Tests/ReactTests/RenderTests.swift index e82d872..f59ec20 100644 --- a/Tests/ReactTests/RenderTests.swift +++ b/Tests/ReactTests/RenderTests.swift @@ -239,8 +239,8 @@ final class RenderTests: XCTestCase { let divDom0: JSHTMLElement = try XCTUnwrap( root.root? - .find { $0.tagElement?.tagName == "div" }? - .dom?.asHTMLElement() + .find { $0.htmlElement?.tagName == "div" }? + .instance?.dom?.asHTMLElement() ) let MouseEvent = JSWindow.global.MouseEvent @@ -266,8 +266,8 @@ final class RenderTests: XCTestCase { // update check let divDom1: JSHTMLElement = try XCTUnwrap( root.root? - .find { $0.tagElement?.tagName == "div" }? - .dom?.asHTMLElement() + .find { $0.htmlElement?.tagName == "div" }? + .instance?.dom?.asHTMLElement() ) XCTAssertEqual(divDom0, divDom1) _ = consume divDom1 @@ -289,8 +289,8 @@ final class RenderTests: XCTestCase { // update check let divDom2: JSHTMLElement = try XCTUnwrap( root.root? - .find { $0.tagElement?.tagName == "div" }? - .dom?.asHTMLElement() + .find { $0.htmlElement?.tagName == "div" }? + .instance?.dom?.asHTMLElement() ) XCTAssertEqual(divDom0, divDom2) _ = consume divDom2 diff --git a/Tests/ReactTests/StateHookTests.swift b/Tests/ReactTests/StateHookTests.swift index c947979..44a1ee1 100644 --- a/Tests/ReactTests/StateHookTests.swift +++ b/Tests/ReactTests/StateHookTests.swift @@ -45,8 +45,8 @@ final class StateHookTests: XCTestCase { let btn: JSHTMLElement = try XCTUnwrap( root.root? - .find { $0.tagElement?.tagName == "button" }? - .dom?.asHTMLElement() + .find { $0.htmlElement?.tagName == "button" }? + .instance?.dom?.asHTMLElement() ) try btn.click() diff --git a/Tests/ReactTests/VNodeTests.swift b/Tests/ReactTests/VNodeTests.swift index f6d5d95..d6ddbee 100644 --- a/Tests/ReactTests/VNodeTests.swift +++ b/Tests/ReactTests/VNodeTests.swift @@ -2,6 +2,25 @@ import XCTest import SRTTestSupport import React +extension VNode { + fileprivate static func tag( + _ name: String, + _ attributes: Attributes = [:], + _ children: [Node] = [] + ) -> VNode { + let tag = HTMLElement( + tagName: name, + attributes: attributes, + children: children + ) + return VNode(component: tag) + } + + fileprivate static func component(_ c: any Component) -> VNode { + VNode(component: c) + } +} + final class VNodeTests: XCTestCase { struct AView: Component { diff --git a/docs/testing.md b/docs/testing.md index d6e588a..d161cbe 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -24,7 +24,15 @@ $ bin/test ### Host Execution Mode via Swift CLI -Like a typical Swift Package, you execute tests on the host machine with the following command: +In this mode, tests connect to a mock library that emulates the DOM. + +To enable the mock, please edit Package.swift as follows: + +```swift +let usesJavaScriptKitMockOnMac = true +``` + +After that, just like a normal Swift package, you execute tests on the host machine with the following command: ```sh $ swift test