From 4ce6e54e161209dcfc20d914610578bb9a45425d Mon Sep 17 00:00:00 2001 From: omochimetaru Date: Wed, 24 Apr 2024 21:44:07 +0900 Subject: [PATCH] updated --- Package.swift | 2 +- Sources/React/Hooks/Effect.swift | 5 ++ Sources/React/Renderer/Instance.swift | 18 +++++- Sources/React/Renderer/ReactRoot.swift | 77 +++++++++++++------------- Sources/React/VDOM/VNode.swift | 2 +- Sources/React/VDOM/VNodeEx.swift | 24 +++----- Tests/ReactTests/RefHookTests.swift | 8 +-- Tests/ReactTests/RenderPlanTests.swift | 8 +-- Tests/ReactTests/RenderTests.swift | 12 ++-- Tests/ReactTests/StateHookTests.swift | 4 +- Tests/ReactTests/VNodeTests.swift | 19 +++++++ docs/testing.md | 10 +++- 12 files changed, 114 insertions(+), 75 deletions(-) diff --git a/Package.swift b/Package.swift index 67cab4c..781c14f 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // for development -let usesJavaScriptKitMockOnMac = false +let usesJavaScriptKitMockOnMac = true let usesLocalJavaScriptKit = false let javaScriptKitDependency: Package.Dependency = usesLocalJavaScriptKit ? 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/Renderer/Instance.swift b/Sources/React/Renderer/Instance.swift index 433e88b..57a1ee8 100644 --- a/Sources/React/Renderer/Instance.swift +++ b/Sources/React/Renderer/Instance.swift @@ -1,6 +1,6 @@ import SRTDOM -internal final class Instance { +package final class Instance { final class ListenerBridge { init() {} @@ -13,13 +13,25 @@ internal final class Instance { } weak var owner: VNode? - var hooks: [any _AnyHookWrapper]? - var dom: JSNode? + 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 { diff --git a/Sources/React/Renderer/ReactRoot.swift b/Sources/React/Renderer/ReactRoot.swift index 1a7a349..20d2cef 100644 --- a/Sources/React/Renderer/ReactRoot.swift +++ b/Sources/React/Renderer/ReactRoot.swift @@ -159,14 +159,14 @@ public final class ReactRoot { if let newTree { let oldInstance = oldTree?.instance oldTree?.instance = nil - let instance = oldInstance ?? Instance() newTree.instance = instance + let isFirst = oldInstance == nil try renderDOM(tree: newTree, instance: instance) try moveDOM(instance: instance) try updateContextValue(tree: newTree, instance: instance) - prepareHooks(component: newTree.component, instance: instance) + prepareHooks(component: newTree.component, instance: instance, isFirst: isFirst) subscribeHooks(instance: instance) // short circuit @@ -180,26 +180,13 @@ public final class ReactRoot { try skipRenderChildren(newTree: newTree, oldTree: oldTree, isMove: isMove) } - if newTree == nil { - if let oldTree { - try oldTree.dom?.remove() - } - } - if let newTree { - for effect in newTree.ghost.effects { - let object = effect.effectObject - if let task = object.taskIfShouldExecute() { - scheduleEffect(task) - } + if let instance = newTree.instance { + try postRender(instance: instance) } } 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) - } + if let instance = oldTree.instance { + try cleanup(instance: instance) } } } @@ -263,17 +250,17 @@ public final class ReactRoot { holder.value = value } - private func prepareHooks(component: any Component, instance: Instance) { + private func prepareHooks(component: any Component, instance: Instance, isFirst: Bool) { let hooks = Components.extractHooks(component) - if let oldHooks = instance.hooks { - for (new, old) in zip(hooks, oldHooks) { - new._prepareAny(object: old.object) - } - } else { + if isFirst { for hook in hooks { hook._prepareAny(object: nil) } + } else { + for (new, old) in zip(hooks, instance.hooks) { + new._prepareAny(object: old.object) + } } instance.hooks = hooks @@ -286,20 +273,18 @@ public final class ReactRoot { self.scheduleUpdate(instance: instance) } - for hook in instance.hooks ?? [] { - switch hook { - case let context as any _AnyContextHook: - 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) - } - case let state as any _AnyStateHook: - state.setDidChange(updater) - default: break + 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) + } } private func isChanged(new: VNode, old: VNode?) -> Bool { @@ -307,6 +292,24 @@ public final class ReactRoot { return newDeps != old?.component.deps } + private func postRender(instance: Instance) throws { + for effect in instance.effectHooks { + if let task = effect.effectObject.taskIfShouldExecute() { + scheduleEffect(task) + } + } + } + + private func cleanup(instance: Instance) throws { + try instance.dom?.remove() + + for effect in instance.effectHooks { + if let task = effect.effectObject.cleanupTask() { + scheduleEffect(task) + } + } + } + private func buildContextValueHolders(for node: VNode) -> [ObjectIdentifier: ContextValueHolder] { var result: [ObjectIdentifier: ContextValueHolder] = [:] diff --git a/Sources/React/VDOM/VNode.swift b/Sources/React/VDOM/VNode.swift index 677ecba..c4e5bf0 100644 --- a/Sources/React/VDOM/VNode.swift +++ b/Sources/React/VDOM/VNode.swift @@ -32,7 +32,7 @@ package final class VNode: Hashable { public let component: any Component public let equality: Equality - internal var instance: Instance? { + package var instance: Instance? { get { _instance } set { _instance = newValue 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