Skip to content

Commit

Permalink
Add a test for environment injection
Browse files Browse the repository at this point in the history
We had some issues in this code area previously and I'm thinking of refactoring it in attempt to fix #367. Would be great to increase the test coverage here before further refactoring.
  • Loading branch information
MaxDesiatov committed Jan 23, 2021
1 parent 9549282 commit cc0f85b
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 37 deletions.
54 changes: 27 additions & 27 deletions Sources/TokamakCore/MountedViews/MountedElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum MountedElementKind {

public class MountedElement<R: Renderer> {
private var element: MountedElementKind
var type: Any.Type { element.type }

public internal(set) var app: _AnyApp {
get {
Expand Down Expand Up @@ -117,20 +118,16 @@ public class MountedElement<R: Renderer> {
updateEnvironment()
}

@discardableResult
func updateEnvironment() -> TypeInfo {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: element.type)
func updateEnvironment() {
let type = element.type
switch element {
case .app:
environmentValues = info.injectEnvironment(from: environmentValues, into: &app.app)
environmentValues.inject(into: &app.app, type)
case .scene:
environmentValues = info.injectEnvironment(from: environmentValues, into: &scene.scene)
environmentValues.inject(into: &scene.scene, type)
case .view:
environmentValues = info.injectEnvironment(from: environmentValues, into: &view.view)
environmentValues.inject(into: &view.view, type)
}

return info
}

func mount(
Expand Down Expand Up @@ -163,59 +160,62 @@ public class MountedElement<R: Renderer> {
}
}

extension TypeInfo {
fileprivate func injectEnvironment(
from environmentValues: EnvironmentValues,
into element: inout Any
) -> EnvironmentValues {
var modifiedEnv = environmentValues
extension EnvironmentValues {
mutating func inject(into element: inout Any, _ type: Any.Type) {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: type)

// swiftlint:disable force_try
// Extract the view from the AnyView for modification, apply Environment changes:
if genericTypes.contains(where: { $0 is EnvironmentModifier.Type }),
let modifier = try! property(named: "modifier").get(from: element) as? EnvironmentModifier
if info.genericTypes.contains(where: { $0 is EnvironmentModifier.Type }),
let modifier = try! info.property(named: "modifier")
.get(from: element) as? EnvironmentModifier
{
modifier.modifyEnvironment(&modifiedEnv)
modifier.modifyEnvironment(&self)
}

// Inject @Environment values
// swiftlint:disable force_cast
// `DynamicProperty`s can have `@Environment` properties contained in them,
// so we have to inject into them as well.
for dynamicProp in properties.filter({ $0.type is DynamicProperty.Type }) {
for dynamicProp in info.properties.filter({ $0.type is DynamicProperty.Type }) {
let propInfo = try! typeInfo(of: dynamicProp.type)
var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty
for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
var wrapper = try! prop.get(from: propWrapper) as! EnvironmentReader
wrapper.setContent(from: modifiedEnv)
wrapper.setContent(from: self)
try! prop.set(value: wrapper, on: &propWrapper)
}
try! dynamicProp.set(value: propWrapper, on: &element)
}
for prop in properties.filter({ $0.type is EnvironmentReader.Type }) {
for prop in info.properties.filter({ $0.type is EnvironmentReader.Type }) {
var wrapper = try! prop.get(from: element) as! EnvironmentReader
wrapper.setContent(from: modifiedEnv)
wrapper.setContent(from: self)
try! prop.set(value: wrapper, on: &element)
}
// swiftlint:enable force_try
// swiftlint:enable force_cast

return modifiedEnv
}
}

extension TypeInfo {
/// Extract all `DynamicProperty` from a type, recursively.
/// This is necessary as a `DynamicProperty` can be nested.
/// `EnvironmentValues` can also be injected at this point.
func dynamicProperties(_ environment: EnvironmentValues, source: inout Any) -> [PropertyInfo] {
func dynamicProperties(
_ environment: inout EnvironmentValues,
source: inout Any
) -> [PropertyInfo] {
var dynamicProps = [PropertyInfo]()
for prop in properties where prop.type is DynamicProperty.Type {
dynamicProps.append(prop)
// swiftlint:disable force_try
let propInfo = try! typeInfo(of: prop.type)
_ = propInfo.injectEnvironment(from: environment, into: &source)
environment.inject(into: &source, prop.type)
var extracted = try! prop.get(from: source)
dynamicProps.append(
contentsOf: propInfo.dynamicProperties(
environment,
&environment,
source: &extracted
)
)
Expand Down
7 changes: 4 additions & 3 deletions Sources/TokamakCore/StackReconciler.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2018-2020 Tokamak contributors
// Copyright 2018-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -208,11 +208,12 @@ public final class StackReconciler<R: Renderer> {
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>
) -> T {
let info = compositeElement.updateEnvironment()
compositeElement.updateEnvironment()
let info = try! typeInfo(of: compositeElement.type)

var stateIdx = 0
let dynamicProps = info.dynamicProperties(
compositeElement.environmentValues,
&compositeElement.environmentValues,
source: &compositeElement[keyPath: bodyKeypath]
)

Expand Down
5 changes: 4 additions & 1 deletion Sources/TokamakDOM/DOMRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ extension EnvironmentValues {
/// Returns default settings for the DOM environment
static var defaultEnvironment: Self {
var environment = EnvironmentValues()

// `.toggleStyle` property is internal
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[_ColorSchemeKey] = .init(matchMediaDarkScheme: matchMediaDarkScheme)

environment.colorScheme = .init(matchMediaDarkScheme: matchMediaDarkScheme)
environment._defaultAppStorage = LocalStorage.standard
_DefaultSceneStorageProvider.default = SessionStorage.standard

Expand Down
12 changes: 9 additions & 3 deletions Sources/TokamakGTK/Views/NavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ protocol GtkStackProtocol {}
// extension NavigationView: AnyWidget, ParentView, GtkStackProtocol {
// var expand: Bool { true }

// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
// func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// let box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)!
// let stack = gtk_stack_new()!
// let sidebar = gtk_stack_sidebar_new()!
Expand Down Expand Up @@ -77,7 +79,9 @@ extension NavigationLink: ViewDeferredToRenderer {
}

// extension NavigationLink: AnyWidget, ParentView {
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
// func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// let btn = gtk_button_new()!
// bindAction(to: btn)
// return btn
Expand Down Expand Up @@ -105,7 +109,9 @@ extension NavigationLink: ViewDeferredToRenderer {
// }

// extension NavigationLink: AnyWidget, ParentView {
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
// func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// print("Creating NavLink widget")
// let btn = gtk_button_new()!
// bindAction(to: btn)
Expand Down
46 changes: 46 additions & 0 deletions Tests/TokamakTests/EnvironmentTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import XCTest

@testable import TokamakCore

private struct TestView: View {
@Environment(\.colorScheme) var colorScheme

public var body: some View {
EmptyView()
}
}

final class EnvironmentTests: XCTestCase {
func testInjection() {
var test: Any = TestView()
var values = EnvironmentValues()
values.colorScheme = .light
values.inject(into: &test, TestView.self)
// swiftlint:disable:next force_cast
XCTAssertEqual((test as! TestView).colorScheme, .light)

values.colorScheme = .dark
values.inject(into: &test, TestView.self)
// swiftlint:disable:next force_cast
XCTAssertEqual((test as! TestView).colorScheme, .dark)

let modifier = TestView().colorScheme(.light)
var anyModifier: Any = modifier

values.inject(into: &anyModifier, type(of: modifier))
XCTAssertEqual(values.colorScheme, .light)
}
}
6 changes: 3 additions & 3 deletions Tests/TokamakTests/ReconcilerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import XCTest

@testable import TokamakCore

struct Counter: View {
private struct Counter: View {
@State var count: Int

let limit: Int

@ViewBuilder public var body: some View {
public var body: some View {
if count < limit {
VStack {
Button("Increment") { count += 1 }
Expand All @@ -37,7 +37,7 @@ struct Counter: View {
}
}

extension Text {
private extension Text {
var verbatim: String? {
guard case let .verbatim(text) = storage else { return nil }
return text
Expand Down

0 comments on commit cc0f85b

Please sign in to comment.